From 2b63a49708a8c09363415f17ff5fc1ee357c3e82 Mon Sep 17 00:00:00 2001 From: robertpitt Date: Mon, 29 Dec 2025 15:42:59 +0000 Subject: [PATCH 1/2] Initial commit of docs sit --- .github/workflows/docs.yml | 54 + .gitignore | 2 + docs-site/.vitepress/config.ts | 56 + docs-site/.vitepress/theme/index.ts | 8 + docs-site/.vitepress/theme/style.scss | 377 +++++++ docs-site/api/index.md | 52 + docs-site/examples/index.md | 35 + docs-site/guide/core-concepts.md | 83 ++ docs-site/guide/getting-started.md | 126 +++ docs-site/index.md | 60 ++ package.json | 11 +- pnpm-lock.yaml | 1432 ++++++++++++++++++++++++- 12 files changed, 2277 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs-site/.vitepress/config.ts create mode 100644 docs-site/.vitepress/theme/index.ts create mode 100644 docs-site/.vitepress/theme/style.scss create mode 100644 docs-site/api/index.md create mode 100644 docs-site/examples/index.md create mode 100644 docs-site/guide/core-concepts.md create mode 100644 docs-site/guide/getting-started.md create mode 100644 docs-site/index.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..e3a47b9 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,54 @@ +name: Deploy Documentation + +on: + release: + types: [published] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build-and-deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build documentation + run: pnpm run docs:build + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs-site/.vitepress/dist + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + diff --git a/.gitignore b/.gitignore index 362d05b..a8aa8fd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ node_modules/ # Build output dist/ +docs-site/.vitepress/dist/ +docs-site/.vitepress/cache/ # Test output coverage/ diff --git a/docs-site/.vitepress/config.ts b/docs-site/.vitepress/config.ts new file mode 100644 index 0000000..df9115d --- /dev/null +++ b/docs-site/.vitepress/config.ts @@ -0,0 +1,56 @@ +import { defineConfig } from 'vitepress'; + +export default defineConfig({ + title: 'itty-spec', + description: 'Contract-first, type-safe API definitions for itty-router', + base: '/itty-spec/', + lastUpdated: true, + ignoreDeadLinks: true, + cleanUrls: true, + markdown: { + theme: { + light: 'github-light', + dark: 'github-dark', + }, + }, + themeConfig: { + nav: [ + { text: 'Guide', link: '/guide/getting-started' }, + { text: 'API', link: '/api/' }, + { text: 'Examples', link: '/examples/' }, + ], + sidebar: { + '/guide/': [ + { + text: 'Getting Started', + items: [ + { text: 'Introduction', link: '/guide/getting-started' }, + { text: 'Core Concepts', link: '/guide/core-concepts' }, + ], + }, + ], + '/api/': [ + { + text: 'API Reference', + items: [{ text: 'Overview', link: '/api/' }], + }, + ], + '/examples/': [ + { + text: 'Examples', + items: [{ text: 'Overview', link: '/examples/' }], + }, + ], + }, + socialLinks: [ + { + icon: 'github', + link: 'https://github.com/robertpitt/itty-spec', + }, + ], + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2024', + }, + }, +}); diff --git a/docs-site/.vitepress/theme/index.ts b/docs-site/.vitepress/theme/index.ts new file mode 100644 index 0000000..a7b2f1c --- /dev/null +++ b/docs-site/.vitepress/theme/index.ts @@ -0,0 +1,8 @@ +// https://vitepress.dev/guide/custom-theme +import type { Theme } from 'vitepress'; +import DefaultTheme from 'vitepress/theme'; +import './style.scss'; + +export default { + extends: DefaultTheme, +} satisfies Theme; diff --git a/docs-site/.vitepress/theme/style.scss b/docs-site/.vitepress/theme/style.scss new file mode 100644 index 0000000..f268f8f --- /dev/null +++ b/docs-site/.vitepress/theme/style.scss @@ -0,0 +1,377 @@ +/** + * Customize default theme styling by overriding CSS variables: + * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css + */ + +/** + * Colors + * + * Each colors have exact same color scale system with 3 levels of solid + * colors with different brightness, and 1 soft color. + * + * - `XXX-1`: The most solid color used mainly for colored text. It must + * satisfy the contrast ratio against when used on top of `XXX-soft`. + * + * - `XXX-2`: The color used mainly for hover state of the button. + * + * - `XXX-3`: The color for solid background, such as bg color of the button. + * It must satisfy the contrast ratio with pure white (#ffffff) text on + * top of it. + * + * - `XXX-soft`: The color used for subtle background such as custom container + * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors + * on top of it. + * + * The soft color must be semi transparent alpha channel. This is crucial + * because it allows adding multiple "soft" colors on top of each other + * to create a accent, such as when having inline code block inside + * custom containers. + * + * - `default`: The color used purely for subtle indication without any + * special meanings attched to it such as bg color for menu hover state. + * + * - `brand`: Used for primary brand colors, such as link text, button with + * brand theme, etc. + * + * - `tip`: Used to indicate useful information. The default theme uses the + * brand color for this by default. + * + * - `warning`: Used to indicate warning to the users. Used in custom + * container, badges, etc. + * + * - `danger`: Used to show error, or dangerous message to the users. Used + * in custom container, badges, etc. + * -------------------------------------------------------------------------- */ + + :root { + --accent-color-p3: color(display-p3 1 0 0.81); + --accent-color: var(--accent-color-p3, #f0c); + + --vp-c-default-1: var(--vp-c-gray-1); + --vp-c-default-2: var(--vp-c-gray-2); + --vp-c-default-3: var(--vp-c-gray-3); + --vp-c-default-soft: var(--vp-c-gray-soft); + + --vp-c-brand-1: var(--vp-c-indigo-1); + --vp-c-brand-2: var(--vp-c-indigo-2); + --vp-c-brand-3: var(--vp-c-indigo-3); + --vp-c-brand-soft: var(--vp-c-indigo-soft); + + --vp-c-tip-1: var(--vp-c-brand-1); + --vp-c-tip-2: var(--vp-c-brand-2); + --vp-c-tip-3: var(--vp-c-brand-3); + --vp-c-tip-soft: var(--vp-c-brand-soft); + + --vp-c-warning-1: var(--vp-c-yellow-1); + --vp-c-warning-2: var(--vp-c-yellow-2); + --vp-c-warning-3: var(--vp-c-yellow-3); + --vp-c-warning-soft: var(--vp-c-yellow-soft); + + --vp-c-danger-1: var(--vp-c-red-1); + --vp-c-danger-2: var(--vp-c-red-2); + --vp-c-danger-3: var(--vp-c-red-3); + --vp-c-danger-soft: var(--vp-c-red-soft); + + // color adjustments + --vp-c-text-1: rgb(30, 30, 33); +} + +html.dark { + --vp-c-text-1: rgba(231,233,230); +} + +/** + * Component: Button + * -------------------------------------------------------------------------- */ + +:root { + --vp-button-brand-border: transparent; + --vp-button-brand-text: var(--vp-c-white); + --vp-button-brand-bg: var(--vp-c-brand-3); + // --vp-button-brand-bg: var(--accent-color); + --vp-button-brand-hover-border: transparent; + --vp-button-brand-hover-text: var(--vp-c-white); + --vp-button-brand-hover-bg: var(--vp-c-brand-2); + --vp-button-brand-active-border: transparent; + --vp-button-brand-active-text: var(--vp-c-white); + --vp-button-brand-active-bg: var(--vp-c-brand-1); +} + +/** + * Component: Home + * -------------------------------------------------------------------------- */ + +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient( + 120deg, + #bd34fe 30%, + #41d1ff + ); + + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + #bd34fe 50%, + #47caff 50% + ); + --vp-home-hero-image-filter: blur(44px); +} + +@media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + +@media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(68px); + } +} + +.VPNav .title, +.accent, +.VPSidebarItem.level-0.is-active > div:first-child a:not(#foo) *, +.VPSidebarItem.level-1.is-active > div:first-child a:not(#foo) *, +.VPSidebarItem.level-1.is-active > div > a:not(#foo) *, +.VPSidebarItem.level-2.is-active > div > a:not(#foo) *, +.VPSidebarItem.level-3.is-active a:not(#foo) * +{ + color: var(--accent-color); +} + +/** + * Component: Custom Block + * -------------------------------------------------------------------------- */ + +:root { + --vp-custom-block-tip-border: transparent; + --vp-custom-block-tip-text: var(--vp-c-text-1); + --vp-custom-block-tip-bg: var(--vp-c-brand-soft); + --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); +} + +/** + * Component: Algolia + * -------------------------------------------------------------------------- */ + +.DocSearch { + --docsearch-primary-color: var(--vp-c-brand-1) !important; +} + +.vp-doc blockquote { + // font-family: Georgia, "Times New Roman", Times, serif; + // font-style: italic; + // border-left: 4px solid var(--accent-color); + // padding: 0.8rem 0 0.7rem 1.5rem; + + font-family: Georgia, "Times New Roman", Times, serif; + quotes: "“" "”" "‘" "’"; + font-size: 1.5rem; + line-height: 1.4em; + font-style: italic; + letter-spacing: -0.01em; + background: var(--vp-c-default-soft); + border-left: .5rem solid var(--vp-c-default-1); + margin: 1.5em 0px; + padding: 1em 1em 1em; + + & > p:first-child:before { + color: #ccc; + color: var(--vp-c-text-3); + content: open-quote; + font-size: 4em; + line-height: 0; + margin-right: 0.15em; + vertical-align: -0.4em; + } +} + +cite { + text-transform: uppercase; + display: flex; + justify-content: flex-end; + margin-top: 0.7em; + color: var(--vp-c-text-2); + font-size: 0.6em; + letter-spacing: -0.01em; + white-space: nowrap; + + &:before { + content: '~ '; + } +} + +.vp-doc blockquote > p { + font-size: 1em; + letter-spacing: -0.03em; + line-height: 1.2em; + font-weight: 400; + color: var(--vp-c-text-1); + +} + +.vp-doc hr { + margin: 3rem 0 2rem; + border-top: 3px dotted var(--vp-c-divider) +} + +.VPSidebarItem:not(.is-link) + .VPSidebarItem.is-link { + margin-top: 1rem; +} + +.VPSidebarItem.level-1:not(:has(a)) p:not(#foo) { + color: var(--vp-c-text-2); + font-family: Georgia, 'Times New Roman', Times, serif; + font-style: italic; +} + +.VPSidebarItem h2:not(#foo) { + font-size: 1.2rem; + letter-spacing: -0.05em; + font-weight: 500; +} + +.vp-doc h2 { + font-size: 1.8rem; +} + +h2 > .VPBadge:not(#foo) { + vertical-align: middle; +} + +h2 > .VPBadge.tip { + background-color:rgba(255,0,200,0.2); + color: var(--vp-c-text-1); + font-size: 0.8rem; +} + +.vp-doc h1:not(#foo) { + font-size: clamp(2.7rem, 12vw, 3.8rem); + letter-spacing: -0.04em; + margin-bottom: 0.6em; + line-height: 0.8em; +} + +.vp-doc h2 { + border-top: none; + padding-top: 0; +} + +.vp-doc p > a > img { + display: inline-block; + margin-right: 0.3em; +} + +.hero { + font-size: 5rem; + letter-spacing: -0.08em; + line-height: 0.8em; + margin: 1em 0; +} + +.image-bg { + opacity: 0.2; +} + +.VPHero h1 em { + -webkit-text-fill-color: var(--accent-color); + font-style: normal; +} + +.VPNavBarTitle span:after { + content: '.dev'; + color: var(--vp-c-text-2); + color: var(--vp-home-hero-name-color); +} + +.VPNavBarTitle span { + background-clip: text !important; + background: var(--vp-home-hero-name-background); +} + +h1 .VPBadge:not(#foo) { + letter-spacing: 0; + vertical-align: middle; +} + +.nowrap { + white-space: nowrap; +} + +// stacked h3+code signatures +.vp-doc h3:has(code) + h3:has(code) { + margin-top: 0; + + &:after { + content: '(alternative)'; + font-size: 0.7em; + font-weight: 400; + } +} + +// h2 + h3+code signature +.vp-doc h2 + h3:has(code) { + margin-top: 0; +} + +.VPBadge > p { + font-size: 0.8rem; + line-height: 1.6em; + margin: 1em 0.6em; +} + +.VPBadge > p+p { + margin-top: 1.8em; +} + +.VPSidebarItem .VPBadge { + letter-spacing: 0; + font-size: 0.6em; +} + +.VPBadge.new { + color: var(--vp-badge-warning-text); + background-color: var(--vp-badge-warning-bg); +} + +.VPSidebarItem.level-0.is-active .VPBadge { + color: var(--vp-badge-warning-text) !important; +} + +.VPSidebarItem.level-1.collapsible { + padding: 0.4rem 0; +} + +.VPSidebarItem.level-1 h3:not(#foo) { + color: var(--vp-c-text-1); + font-weight: 600; +} + +.vp-doc h3:first-child { + margin-top: 0; +} + +.VPSidebarItem.level-0:first-child { + padding-bottom: 0.7rem; +} + +.vp-adaptive-theme + blockquote { + font-size: 0.85em; + background-color: transparent; + border-left: none; + padding-left: 0; + margin-top: -0.8rem; + text-align: right; + + & > p:first-child:before { + content: ''; + font-size: inherit; + vertical-align: inherit; + color: inherit; + } + + & > p:first-child:has(a):before { + content: 'references: '; + } +} \ No newline at end of file diff --git a/docs-site/api/index.md b/docs-site/api/index.md new file mode 100644 index 0000000..e89359a --- /dev/null +++ b/docs-site/api/index.md @@ -0,0 +1,52 @@ +# API Reference + +## createContract + +Creates a contract object that defines your API operations. + +```ts +import { createContract } from "itty-spec"; + +const contract = createContract({ + operationName: { + path: "/path", + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", + // ... operation definition + }, +}); +``` + +## createRouter + +Creates a router with handlers bound to a contract. + +```ts +import { createRouter } from "itty-spec"; + +const router = createRouter({ + contract, + handlers: { + operationName: async (request) => { + // Handler implementation + }, + }, +}); +``` + +## createOpenApiSpecification + +Generates an OpenAPI 3.1 specification from a contract. + +```ts +import { createOpenApiSpecification } from "itty-spec/openapi"; + +const spec = await createOpenApiSpecification(contract, { + title: "API Title", + version: "1.0.0", + description: "API Description", + servers: [{ url: "https://api.example.com" }], +}); +``` + +For detailed API documentation, refer to the TypeScript definitions in the source code. + diff --git a/docs-site/examples/index.md b/docs-site/examples/index.md new file mode 100644 index 0000000..55b2137 --- /dev/null +++ b/docs-site/examples/index.md @@ -0,0 +1,35 @@ +# Examples + +This project includes several example implementations to help you get started: + +## Simple Example + +A basic example showing how to define a contract and create a router with handlers. + +Location: `examples/simple/` + +## Complex Example + +A more comprehensive example demonstrating: +- Multiple contracts organized by domain +- Authentication middleware +- Pagination utilities +- Database integration patterns +- OpenAPI specification generation + +Location: `examples/complex/` + +## Valibot Example + +An example using Valibot instead of Zod for schema validation. + +Location: `examples/valibot/` + +## Repository Layout + +* `src/` - Library source code +* `examples/` - Usage examples +* `tests/` - Test suite + +For complete working examples, check out the [examples directory](https://github.com/robertpitt/itty-spec/tree/main/examples) in the repository. + diff --git a/docs-site/guide/core-concepts.md b/docs-site/guide/core-concepts.md new file mode 100644 index 0000000..9476370 --- /dev/null +++ b/docs-site/guide/core-concepts.md @@ -0,0 +1,83 @@ +# Core Concepts + +## Contract + +A contract is a plain object describing each operation: + +* `method` and `path` +* optional schemas for `path params`, `query`, `headers`, and request bodies +* allowed `responses` keyed by status code and content type + +The contract drives both runtime behavior (validation + routing) and compile-time types. + +## Router + +`createRouter({ contract, handlers })` binds your handlers to the contract and produces a Fetch handler (`router.fetch`). + +Before a handler is called, `itty-spec` validates the incoming request according to the schemas you provided. Your handler receives a request object with typed, validated data (for example `request.validatedQuery` and `request.validatedBody`). + +## Responses + +Handlers return responses via `request.respond({ status, contentType, body })`. + +The shape of that response is type-checked against the contract for the current operation, so returning the wrong status code, content type, or body shape becomes a TypeScript error. + +## Schema Support + +`itty-spec` uses the [Standard Schema V1](https://github.com/standard-schema/spec) interface, which provides a common abstraction layer for schema validation. This means you can use any Standard Schema V1 compatible library: + +* **Zod (v4)**: Fully supported with excellent TypeScript inference and OpenAPI generation. Recommended for the best developer experience. +* **Valibot**: Fully supported with OpenAPI generation via `@standard-community/standard-openapi`. +* **Other Standard Schema compatible libraries**: Can be used for validation; OpenAPI support depends on the library's Standard Schema V1 implementation. + +The Standard Schema V1 interface ensures that your contracts remain portable across different schema libraries while maintaining type safety and runtime validation. + +## OpenAPI 3.1 Generation and Serving (Optional) + +Generate an OpenAPI 3.1 specification directly from your contract and serve it as a documentation endpoint: + +```ts +import { createOpenApiSpecification } from "itty-spec/openapi"; +import { createRouter } from "itty-spec"; +import { contract } from "./contract"; +import { z } from "zod"; + +// Generate the OpenAPI spec +const openApiSpec = await createOpenApiSpecification(contract, { + title: "My API", + version: "1.0.0", + description: "Example API built with itty-spec", + servers: [{ url: "https://api.example.com", description: "Production" }], +}); + +// Serve it as a route in your router +const router = createRouter({ + contract: { + ...contract, + getSpec: { + path: "/openapi.json", + method: "GET", + responses: { + 200: { + "application/json": { body: z.any() }, + }, + }, + }, + }, + handlers: { + ...yourHandlers, + getSpec: async (request) => { + return request.respond({ + status: 200, + contentType: "application/json", + body: openApiSpec, + }); + }, + }, +}); +``` + +OpenAPI generation uses `@standard-community/standard-openapi` to convert Standard Schema V1 schemas to OpenAPI 3.1 format. This supports Zod v4 and Valibot schemas out of the box. You can then use tools like [Swagger UI](https://swagger.io/tools/swagger-ui/), [Redoc](https://github.com/Redocly/redoc), or [Elements](https://github.com/stoplightio/elements) to render interactive documentation from the served specification. + +See the `examples/simple` and `examples/complex` directories for complete examples of serving OpenAPI documentation. + diff --git a/docs-site/guide/getting-started.md b/docs-site/guide/getting-started.md new file mode 100644 index 0000000..5c84e0f --- /dev/null +++ b/docs-site/guide/getting-started.md @@ -0,0 +1,126 @@ +# Getting Started + +## Installation + +```bash +npm install itty-spec +# or +pnpm add itty-spec +``` + +## Quick Start + +### 1) Define a contract + +```ts +import { createContract } from "itty-spec"; +import { z } from "zod"; + +const UserEntity = z.object({ + id: z.uuid(), + name: z.string().min(1), + email: z.string().email(), + age: z.number().min(18).optional(), +}); + +const CreateUserRequest = z.object({ + name: z.string().min(1), + email: z.string().email(), + age: z.number().min(18).optional(), +}); + +const ListUsersResponse = z.object({ + users: z.array(UserEntity), + total: z.number(), +}); + +export const contract = createContract({ + getUsers: { + path: "/users", + method: "GET", + headers: z.object({ + "x-api-key": z.string(), + }), + query: z.object({ + page: z.number().min(1).default(1), + limit: z.number().min(1).max(100).default(10), + }), + responses: { + 200: { + "application/json": { body: ListUsersResponse }, + }, + }, + }, + + createUser: { + path: "/users", + method: "POST", + headers: z.object({ + "x-api-key": z.string(), + }), + requests: { + "application/json": { + body: CreateUserRequest, + }, + }, + responses: { + 200: { + "application/json": { body: UserEntity }, + }, + 400: { + "application/json": { body: z.object({ error: z.string() }) }, + }, + }, + }, +}); +``` + +### 2) Implement the contract with a router + +```ts +import { createRouter } from "itty-spec"; +import { contract } from "./contract"; + +const router = createRouter({ + contract, + handlers: { + getUsers: async (request) => { + const { page, limit } = request.validatedQuery; + + return request.respond({ + status: 200, + contentType: "application/json", + body: { users: [], total: 0 }, + }); + }, + + createUser: async (request) => { + const { name, email } = request.validatedBody; + + return request.respond({ + status: 200, + contentType: "application/json", + body: { id: "123", name, email }, + }); + }, + }, +}); + +export default { + fetch: router.fetch, +}; +``` + +## Target Environments + +`itty-spec` is designed to be lightweight and efficient, making it ideal for: + +- **Cloudflare Workers**: Edge computing with minimal cold start times +- **AWS Lambda**: Serverless functions with size constraints +- **Node.js servers**: Traditional backend servers +- **Bun**: Fast JavaScript runtime +- **Deno**: Secure runtime for JavaScript and TypeScript +- **Any Fetch-compatible environment**: Works wherever the Fetch API is available + +The library's minimal dependencies and small bundle size ensure fast startup times and low memory footprint, critical for edge and serverless deployments. + diff --git a/docs-site/index.md b/docs-site/index.md new file mode 100644 index 0000000..0d59bb3 --- /dev/null +++ b/docs-site/index.md @@ -0,0 +1,60 @@ +--- +layout: home + +hero: + name: itty-spec + text: Contract-first, type-safe API definitions + tagline: for itty-router + actions: + - theme: brand + text: Get Started + link: /guide/getting-started + - theme: alt + text: View on GitHub + link: https://github.com/robertpitt/itty-spec + +features: + - title: Contract-first API design + details: Define routes, inputs, and outputs once. The contract drives both runtime behavior and compile-time types. + - title: Fully typed TypeScript + details: Complete type inference from contract to handler, with compile-time guarantees for request/response shapes. + - title: Runtime validation + details: Invalid requests are rejected before your handler runs, using Standard Schema V1 compatible validators. + - title: End-to-end type safety + details: Handlers receive typed, validated data (validatedParams, validatedQuery, validatedBody, validatedHeaders). + - title: Typed response builder + details: Responses must match the contract - TypeScript errors catch mismatches at compile time. + - title: Fetch-first compatibility + details: Works in any environment that supports the Fetch API - Cloudflare Workers, AWS Lambda, Node.js, Bun, Deno. + - title: OpenAPI generation + details: Generate and serve OpenAPI 3.1 specifications from the same contract using @standard-community/standard-openapi. + - title: Minimal bundle size + details: Designed for edge/serverless environments where every byte counts. +--- + +## What this project provides + +- **Contract-first API design**: define routes, inputs, and outputs once. +- **Fully typed TypeScript experience**: complete type inference from contract to handler, with compile-time guarantees for request/response shapes. +- **Runtime validation**: invalid requests are rejected before your handler runs, using Standard Schema V1 compatible validators. +- **End-to-end TypeScript inference**: handlers receive typed, validated data (`validatedParams`, `validatedQuery`, `validatedBody`, `validatedHeaders`). +- **Typed response builder**: responses must match the contract (status/content-type/body) - TypeScript errors catch mismatches at compile time. +- **Fetch-first compatibility**: works in any environment that supports the Fetch API. +- **OpenAPI generation and serving**: generate and serve OpenAPI 3.1 specifications from the same contract using `@standard-community/standard-openapi`. + +## What this project is not + +- Not a full application framework (no controllers, DI container, ORM, etc.). +- Not a server runtime (you bring your own deployment: Workers, Node, Bun, Deno, etc.). +- Not a replacement for itty-router; it builds on it. + +## Foundation + +`itty-spec` is built on a lightweight foundation of battle-tested libraries: + +- **[itty-router](https://itty.dev/itty-router)** (v5): The tiny router for Fetch that powers routing and request handling. +- **[Standard Schema V1](https://github.com/standard-schema/spec)** (`@standard-schema/spec`): Provides a common interface for schema validation, enabling compatibility with multiple schema libraries. +- **[Standard Community OpenAPI](https://github.com/standard-community/standard-openapi)** (`@standard-community/standard-openapi`): Converts Standard Schema V1 schemas to OpenAPI 3.1 format for documentation and tooling. + +This architecture ensures minimal bundle size while providing maximum type safety and developer experience. The library is designed to work seamlessly in edge/serverless environments where every byte counts. + diff --git a/package.json b/package.json index b120818..7418e53 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,10 @@ "test:types": "vitest --typecheck", "prepublishOnly": "pnpm run build", "prepare": "husky", - "publish:npm": "pnpm publish --access public" + "publish:npm": "pnpm publish --access public", + "docs:dev": "vitepress dev docs-site", + "docs:build": "vitepress build docs-site", + "docs:preview": "vitepress preview docs-site" }, "keywords": [ "itty-router", @@ -69,17 +72,19 @@ "@types/bun": "^1.3.4", "@types/node": "^25.0.1", "@valibot/to-json-schema": "^1.5.0", - "zod-openapi": "^4.0.0", "@vitest/coverage-v8": "^4.0.15", "@whatwg-node/server": "^0.10.17", "husky": "^9.1.7", "oxfmt": "^0.17.0", + "sass": "^1.97.1", "tsdown": "^0.17.0", "tsx": "^4.21.0", "typescript": "^5.0.0", "valibot": "^1.2.0", + "vitepress": "next", "vitest": "^4.0.15", - "zod": "v4" + "zod": "v4", + "zod-openapi": "^4.0.0" }, "dependencies": { "@standard-community/standard-openapi": "^0.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98a177a..20af47d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,7 +32,7 @@ importers: version: 1.5.0(valibot@1.2.0(typescript@5.9.3)) '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(tsx@4.21.0)) + version: 4.0.15(vitest@4.0.15(@opentelemetry/api@1.9.0)(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0)) '@whatwg-node/server': specifier: ^0.10.17 version: 0.10.17 @@ -42,6 +42,9 @@ importers: oxfmt: specifier: ^0.17.0 version: 0.17.0 + sass: + specifier: ^1.97.1 + version: 1.97.1 tsdown: specifier: ^0.17.0 version: 0.17.3(typescript@5.9.3) @@ -54,9 +57,12 @@ importers: valibot: specifier: ^1.2.0 version: 1.2.0(typescript@5.9.3) + vitepress: + specifier: next + version: 2.0.0-alpha.15(@algolia/client-search@5.46.2)(@types/node@25.0.2)(postcss@8.5.6)(react@19.2.3)(sass@1.97.1)(search-insights@2.17.3)(tsx@4.21.0)(typescript@5.9.3) vitest: specifier: ^4.0.15 - version: 4.0.15(@types/node@25.0.2)(tsx@4.21.0) + version: 4.0.15(@opentelemetry/api@1.9.0)(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0) zod: specifier: v4 version: 4.1.13 @@ -66,6 +72,102 @@ importers: packages: + '@ai-sdk/gateway@2.0.23': + resolution: {integrity: sha512-qmX7afPRszUqG5hryHF3UN8ITPIRSGmDW6VYCmByzjoUkgm3MekzSx2hMV1wr0P+llDeuXb378SjqUfpvWJulg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@3.0.19': + resolution: {integrity: sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@2.0.0': + resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + engines: {node: '>=18'} + + '@ai-sdk/react@2.0.118': + resolution: {integrity: sha512-K/5VVEGTIu9SWrdQ0s/11OldFU8IjprDzeE6TaC2fOcQWhG7dGVGl9H8Z32QBHzdfJyMhFUxEyFKSOgA2j9+VQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 + zod: ^3.25.76 || ^4.1.8 + peerDependenciesMeta: + zod: + optional: true + + '@algolia/abtesting@1.12.2': + resolution: {integrity: sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/autocomplete-core@1.19.2': + resolution: {integrity: sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==} + + '@algolia/autocomplete-plugin-algolia-insights@1.19.2': + resolution: {integrity: sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-shared@1.19.2': + resolution: {integrity: sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.46.2': + resolution: {integrity: sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.46.2': + resolution: {integrity: sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.46.2': + resolution: {integrity: sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.46.2': + resolution: {integrity: sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.46.2': + resolution: {integrity: sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.46.2': + resolution: {integrity: sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.46.2': + resolution: {integrity: sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.46.2': + resolution: {integrity: sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.46.2': + resolution: {integrity: sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.46.2': + resolution: {integrity: sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.46.2': + resolution: {integrity: sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.46.2': + resolution: {integrity: sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.46.2': + resolution: {integrity: sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==} + engines: {node: '>= 14.0.0'} + '@babel/generator@7.28.5': resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} @@ -91,6 +193,43 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@docsearch/core@4.4.0': + resolution: {integrity: sha512-kiwNo5KEndOnrf5Kq/e5+D9NBMCFgNsDoRpKQJ9o/xnSlheh6b8AXppMuuUVVdAUIhIfQFk/07VLjjk/fYyKmw==} + peerDependencies: + '@types/react': '>= 16.8.0 < 20.0.0' + react: '>= 16.8.0 < 20.0.0' + react-dom: '>= 16.8.0 < 20.0.0' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + + '@docsearch/css@4.4.0': + resolution: {integrity: sha512-e9vPgtih6fkawakmYo0Y6V4BKBmDV7Ykudn7ADWXUs5b6pmtBRwDbpSG/WiaUG63G28OkJDEnsMvgIAnZgGwYw==} + + '@docsearch/js@4.4.0': + resolution: {integrity: sha512-vCiKzjYD54bugUIMZA6YzuLDilkD3TNH/kfbvqsnzxiLTMu8F13psD+hdMSEOn7j+dFJOaf49fZ+gwr+rXctMw==} + + '@docsearch/react@4.4.0': + resolution: {integrity: sha512-z12zeg1mV7WD4Ag4pKSuGukETJLaucVFwszDXL/qLaEgRqxEaVacO9SR1qqnCXvZztlvz2rt7cMqryi/7sKfjA==} + peerDependencies: + '@types/react': '>= 16.8.0 < 20.0.0' + react: '>= 16.8.0 < 20.0.0' + react-dom: '>= 16.8.0 < 20.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -419,6 +558,12 @@ packages: '@fastify/busboy@3.2.0': resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@iconify-json/simple-icons@1.2.64': + resolution: {integrity: sha512-SMmm//tjZBvHnT0EAzZLnBTL6bukSkncM0pwkOXjr0FsAeCqjQtqoxBR0Mp+PazIJjXJKHm1Ju0YgnCIPOodJg==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -435,6 +580,10 @@ packages: '@napi-rs/wasm-runtime@1.1.0': resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@oxc-project/types@0.101.0': resolution: {integrity: sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==} @@ -478,6 +627,88 @@ packages: cpu: [x64] os: [win32] + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} @@ -671,6 +902,30 @@ packages: cpu: [x64] os: [win32] + '@shikijs/core@3.20.0': + resolution: {integrity: sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g==} + + '@shikijs/engine-javascript@3.20.0': + resolution: {integrity: sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg==} + + '@shikijs/engine-oniguruma@3.20.0': + resolution: {integrity: sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==} + + '@shikijs/langs@3.20.0': + resolution: {integrity: sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==} + + '@shikijs/themes@3.20.0': + resolution: {integrity: sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==} + + '@shikijs/transformers@3.20.0': + resolution: {integrity: sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g==} + + '@shikijs/types@3.20.0': + resolution: {integrity: sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@standard-community/standard-json@0.3.5': resolution: {integrity: sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==} peerDependencies: @@ -750,17 +1005,52 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node@25.0.2': resolution: {integrity: sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@valibot/to-json-schema@1.5.0': resolution: {integrity: sha512-GE7DmSr1C2UCWPiV0upRH6mv0cCPsqYGs819fb6srCS1tWhyXrkGGe+zxUiwzn/L1BOfADH4sNjY/YHCuP8phQ==} peerDependencies: valibot: ^1.2.0 + '@vercel/oidc@3.0.5': + resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==} + engines: {node: '>= 20'} + + '@vitejs/plugin-vue@6.0.3': + resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.2.25 + '@vitest/coverage-v8@4.0.15': resolution: {integrity: sha512-FUJ+1RkpTFW7rQITdgTi93qOCWJobWhBirEPCeXh2SW2wsTlFxy51apDz5gzG+ZEYt/THvWeNmhdAoS9DTwpCw==} peerDependencies: @@ -799,6 +1089,99 @@ packages: '@vitest/utils@4.0.15': resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==} + '@vue/compiler-core@3.5.26': + resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==} + + '@vue/compiler-dom@3.5.26': + resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==} + + '@vue/compiler-sfc@3.5.26': + resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==} + + '@vue/compiler-ssr@3.5.26': + resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} + + '@vue/devtools-api@8.0.5': + resolution: {integrity: sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==} + + '@vue/devtools-kit@8.0.5': + resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==} + + '@vue/devtools-shared@8.0.5': + resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==} + + '@vue/reactivity@3.5.26': + resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==} + + '@vue/runtime-core@3.5.26': + resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==} + + '@vue/runtime-dom@3.5.26': + resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==} + + '@vue/server-renderer@3.5.26': + resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==} + peerDependencies: + vue: 3.5.26 + + '@vue/shared@3.5.26': + resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} + + '@vueuse/core@14.1.0': + resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/integrations@14.1.0': + resolution: {integrity: sha512-eNQPdisnO9SvdydTIXnTE7c29yOsJBD/xkwEyQLdhDC/LKbqrFpXHb3uS//7NcIrQO3fWVuvMGp8dbK6mNEMCA==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 || ^8 + vue: ^3.5.0 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@14.1.0': + resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==} + + '@vueuse/shared@14.1.0': + resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==} + peerDependencies: + vue: ^3.5.0 + '@whatwg-node/disposablestack@0.0.6': resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} engines: {node: '>=18.0.0'} @@ -819,6 +1202,16 @@ packages: resolution: {integrity: sha512-QxI+HQfJeI/UscFNCTcSri6nrHP25mtyAMbhEri7W2ctdb3EsorPuJz7IovSgNjvKVs73dg9Fmayewx1O2xOxA==} engines: {node: '>=18.0.0'} + ai@5.0.116: + resolution: {integrity: sha512-+2hYJ80/NcDWuv9K2/MLP3cTCFgwWHmHlS1tOpFUKKcmLbErAAlE/S2knsKboc3PNAu8pQkDr2N3K/Vle7ENgQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + algoliasearch@5.46.2: + resolution: {integrity: sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q==} + engines: {node: '>= 14.0.0'} + ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} @@ -834,9 +1227,16 @@ packages: ast-v8-to-istanbul@0.3.8: resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==} + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + birpc@3.0.0: resolution: {integrity: sha512-by+04pHuxpCEQcucAXqzopqfhyI8TLK5Qg5MST0cB6MP+JhHna9ollrtK9moVh27aq6Q6MEJgebD0cVm//yBkg==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + bun-types@1.3.4: resolution: {integrity: sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ==} @@ -844,10 +1244,33 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.1: resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -860,6 +1283,18 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dts-resolver@2.1.3: resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} engines: {node: '>=20.19.0'} @@ -873,6 +1308,10 @@ packages: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} + entities@7.0.0: + resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} + engines: {node: '>=0.12'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -886,9 +1325,16 @@ packages: engines: {node: '>=18'} hasBin: true + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -902,6 +1348,13 @@ packages: picomatch: optional: true + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + focus-trap@7.7.0: + resolution: {integrity: sha512-DJJDHpEgoSbP8ZE1MNeU2IzCpfFyFdNZZRilqmfH2XiQsPK6PtD8AfJqWzEBudUQB2yHwZc5iq54rjTaGQ+ljw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -914,21 +1367,52 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + htm@3.1.1: + resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} hasBin: true + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + import-without-cache@0.2.3: resolution: {integrity: sha512-roCvX171VqJ7+7pQt1kSRfwaJvFAC2zhThJWXal1rN8EqzPS3iapkAoNpHh4lM8Na1BDen+n9rVfo73RN+Y87g==} engines: {node: '>=20.19.0'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -956,6 +1440,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -966,6 +1453,42 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -974,9 +1497,18 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.4: + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -988,9 +1520,16 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@2.0.0: + resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -999,12 +1538,35 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown-plugin-dts@0.18.3: resolution: {integrity: sha512-rd1LZ0Awwfyn89UndUF/HoFF4oH9a5j+2ZeuKSJYM80vmeN/p0gslYMnHTQHBEXPhUlvAlqGA3tVgXB/1qFNDg==} engines: {node: '>=20.19.0'} @@ -1034,11 +1596,22 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + sass@1.97.1: + resolution: {integrity: sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==} + engines: {node: '>=14.0.0'} + hasBin: true + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true + shiki@3.20.0: + resolution: {integrity: sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1046,16 +1619,42 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + swr@2.3.8: + resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + tabbable@6.3.0: + resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==} + + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1071,10 +1670,17 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + tsdown@0.17.3: resolution: {integrity: sha512-bgLgTog+oyadDTr9SZ57jZtb+A4aglCjo3xgJrkCDxbzcQl2l2iDDr4b06XHSQHwyDNIhYFDgPRhuu1wL3pNsw==} engines: {node: '>=20.19.0'} @@ -1119,6 +1725,21 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unrun@0.2.19: resolution: {integrity: sha512-DbwbJ9BvPEb3BeZnIpP9S5tGLO/JIgPQ3JrpMRFIfZMZfMG19f26OlLbC2ml8RRdrI2ZA7z2t+at5tsIHbh6Qw==} engines: {node: '>=20.19.0'} @@ -1132,6 +1753,11 @@ packages: urlpattern-polyfill@10.1.0: resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + valibot@1.2.0: resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: @@ -1140,6 +1766,12 @@ packages: typescript: optional: true + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@7.2.7: resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1180,6 +1812,21 @@ packages: yaml: optional: true + vitepress@2.0.0-alpha.15: + resolution: {integrity: sha512-jhjSYd10Z6RZiKOa7jy0xMVf5NB5oSc/lS3bD/QoUc6V8PrvQR5JhC9104NEt6+oTGY/ftieVWxY9v7YI+1IjA==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + oxc-minify: '*' + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + oxc-minify: + optional: true + postcss: + optional: true + vitest@4.0.15: resolution: {integrity: sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1214,6 +1861,14 @@ packages: jsdom: optional: true + vue@3.5.26: + resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -1228,15 +1883,152 @@ packages: zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: - '@babel/generator@7.28.5': + '@ai-sdk/gateway@2.0.23(zod@4.1.13)': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.19(zod@4.1.13) + '@vercel/oidc': 3.0.5 + zod: 4.1.13 + + '@ai-sdk/provider-utils@3.0.19(zod@4.1.13)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.6 + zod: 4.1.13 + + '@ai-sdk/provider@2.0.0': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@2.0.118(react@19.2.3)(zod@4.1.13)': + dependencies: + '@ai-sdk/provider-utils': 3.0.19(zod@4.1.13) + ai: 5.0.116(zod@4.1.13) + react: 19.2.3 + swr: 2.3.8(react@19.2.3) + throttleit: 2.1.0 + optionalDependencies: + zod: 4.1.13 + + '@algolia/abtesting@1.12.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/autocomplete-core@1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-shared@1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)': + dependencies: + '@algolia/client-search': 5.46.2 + algoliasearch: 5.46.2 + + '@algolia/client-abtesting@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/client-analytics@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/client-common@5.46.2': {} + + '@algolia/client-insights@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/client-personalization@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/client-query-suggestions@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/client-search@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/ingestion@1.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/monitoring@1.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/recommend@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/requester-browser-xhr@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + + '@algolia/requester-fetch@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + + '@algolia/requester-node-http@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 '@babel/helper-string-parser@7.27.1': {} @@ -1253,6 +2045,39 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@docsearch/core@4.4.0(react@19.2.3)': + optionalDependencies: + react: 19.2.3 + + '@docsearch/css@4.4.0': {} + + '@docsearch/js@4.4.0(@algolia/client-search@5.46.2)(react@19.2.3)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 4.4.0(@algolia/client-search@5.46.2)(react@19.2.3)(search-insights@2.17.3) + htm: 3.1.1 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + + '@docsearch/react@4.4.0(@algolia/client-search@5.46.2)(react@19.2.3)(search-insights@2.17.3)': + dependencies: + '@ai-sdk/react': 2.0.118(react@19.2.3)(zod@4.1.13) + '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.46.2)(algoliasearch@5.46.2)(search-insights@2.17.3) + '@docsearch/core': 4.4.0(react@19.2.3) + '@docsearch/css': 4.4.0 + ai: 5.0.116(zod@4.1.13) + algoliasearch: 5.46.2 + marked: 16.4.2 + zod: 4.1.13 + optionalDependencies: + react: 19.2.3 + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -1432,6 +2257,12 @@ snapshots: '@fastify/busboy@3.2.0': {} + '@iconify-json/simple-icons@1.2.64': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1453,6 +2284,8 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@opentelemetry/api@1.9.0': {} + '@oxc-project/types@0.101.0': {} '@oxfmt/darwin-arm64@0.17.0': @@ -1479,6 +2312,67 @@ snapshots: '@oxfmt/win32-x64@0.17.0': optional: true + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + '@quansync/fs@1.0.0': dependencies: quansync: 1.0.0 @@ -1592,6 +2486,44 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.53.3': optional: true + '@shikijs/core@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.4 + + '@shikijs/engine-oniguruma@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + + '@shikijs/themes@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + + '@shikijs/transformers@3.20.0': + dependencies: + '@shikijs/core': 3.20.0 + '@shikijs/types': 3.20.0 + + '@shikijs/types@3.20.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3)))(quansync@1.0.0)(valibot@1.2.0(typescript@5.9.3))(zod@4.1.13)': dependencies: '@standard-schema/spec': 1.0.0 @@ -1632,17 +2564,48 @@ snapshots: '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + '@types/node@25.0.2': dependencies: undici-types: 7.16.0 + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.0': {} + '@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3))': dependencies: valibot: 1.2.0(typescript@5.9.3) - '@vitest/coverage-v8@4.0.15(vitest@4.0.15(@types/node@25.0.2)(tsx@4.21.0))': + '@vercel/oidc@3.0.5': {} + + '@vitejs/plugin-vue@6.0.3(vite@7.2.7(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0))(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.53 + vite: 7.2.7(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0) + vue: 3.5.26(typescript@5.9.3) + + '@vitest/coverage-v8@4.0.15(vitest@4.0.15(@opentelemetry/api@1.9.0)(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.15 @@ -1655,7 +2618,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.15(@types/node@25.0.2)(tsx@4.21.0) + vitest: 4.0.15(@opentelemetry/api@1.9.0)(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -1668,13 +2631,13 @@ snapshots: chai: 6.2.1 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.15(vite@7.2.7(@types/node@25.0.2)(tsx@4.21.0))': + '@vitest/mocker@4.0.15(vite@7.2.7(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.0.15 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.7(@types/node@25.0.2)(tsx@4.21.0) + vite: 7.2.7(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0) '@vitest/pretty-format@4.0.15': dependencies: @@ -1698,6 +2661,99 @@ snapshots: '@vitest/pretty-format': 4.0.15 tinyrainbow: 3.0.3 + '@vue/compiler-core@3.5.26': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.26 + entities: 7.0.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.26': + dependencies: + '@vue/compiler-core': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/compiler-sfc@3.5.26': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.26 + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.26': + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/devtools-api@8.0.5': + dependencies: + '@vue/devtools-kit': 8.0.5 + + '@vue/devtools-kit@8.0.5': + dependencies: + '@vue/devtools-shared': 8.0.5 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 2.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@8.0.5': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.26': + dependencies: + '@vue/shared': 3.5.26 + + '@vue/runtime-core@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/shared': 3.5.26 + + '@vue/runtime-dom@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/runtime-core': 3.5.26 + '@vue/shared': 3.5.26 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + vue: 3.5.26(typescript@5.9.3) + + '@vue/shared@3.5.26': {} + + '@vueuse/core@14.1.0(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.1.0 + '@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.9.3)) + vue: 3.5.26(typescript@5.9.3) + + '@vueuse/integrations@14.1.0(focus-trap@7.7.0)(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@vueuse/core': 14.1.0(vue@3.5.26(typescript@5.9.3)) + '@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.9.3)) + vue: 3.5.26(typescript@5.9.3) + optionalDependencies: + focus-trap: 7.7.0 + + '@vueuse/metadata@14.1.0': {} + + '@vueuse/shared@14.1.0(vue@3.5.26(typescript@5.9.3))': + dependencies: + vue: 3.5.26(typescript@5.9.3) + '@whatwg-node/disposablestack@0.0.6': dependencies: '@whatwg-node/promise-helpers': 1.3.2 @@ -1727,6 +2783,31 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.2 tslib: 2.8.1 + ai@5.0.116(zod@4.1.13): + dependencies: + '@ai-sdk/gateway': 2.0.23(zod@4.1.13) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.19(zod@4.1.13) + '@opentelemetry/api': 1.9.0 + zod: 4.1.13 + + algoliasearch@5.46.2: + dependencies: + '@algolia/abtesting': 1.12.2 + '@algolia/client-abtesting': 5.46.2 + '@algolia/client-analytics': 5.46.2 + '@algolia/client-common': 5.46.2 + '@algolia/client-insights': 5.46.2 + '@algolia/client-personalization': 5.46.2 + '@algolia/client-query-suggestions': 5.46.2 + '@algolia/client-search': 5.46.2 + '@algolia/ingestion': 1.46.2 + '@algolia/monitoring': 1.46.2 + '@algolia/recommend': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + ansis@4.2.0: {} assertion-error@2.0.1: {} @@ -1742,26 +2823,62 @@ snapshots: estree-walker: 3.0.3 js-tokens: 9.0.1 + birpc@2.9.0: {} + birpc@3.0.0: {} + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + optional: true + bun-types@1.3.4: dependencies: '@types/node': 25.0.2 cac@6.7.14: {} + ccount@2.0.1: {} + chai@6.2.1: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + comma-separated-tokens@2.0.3: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + csstype@3.2.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 defu@6.1.4: {} + dequal@2.0.3: {} + + detect-libc@1.0.3: + optional: true + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dts-resolver@2.1.3: {} empathic@2.0.0: {} + entities@7.0.0: {} + es-module-lexer@1.7.0: {} esbuild@0.25.12: @@ -1822,16 +2939,29 @@ snapshots: '@esbuild/win32-ia32': 0.27.1 '@esbuild/win32-x64': 0.27.1 + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + eventsource-parser@3.0.6: {} + expect-type@1.3.0: {} fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + optional: true + + focus-trap@7.7.0: + dependencies: + tabbable: 6.3.0 + fsevents@2.3.3: optional: true @@ -1841,14 +2971,51 @@ snapshots: has-flag@4.0.0: {} + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hookable@5.5.3: {} + htm@3.1.1: {} + html-escaper@2.0.2: {} + html-void-elements@3.0.0: {} + husky@9.1.7: {} + immutable@5.1.4: {} + import-without-cache@0.2.3: {} + is-extglob@2.1.1: + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + optional: true + + is-number@7.0.0: + optional: true + + is-what@5.5.0: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -1876,6 +3043,8 @@ snapshots: jsesc@3.1.0: {} + json-schema@0.4.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1890,12 +3059,66 @@ snapshots: dependencies: semver: 7.7.3 + mark.js@8.11.1: {} + + marked@16.4.2: {} + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + optional: true + + minisearch@7.2.0: {} + + mitt@3.0.1: {} + ms@2.1.3: {} nanoid@3.3.11: {} + node-addon-api@7.1.1: + optional: true + obug@2.1.1: {} + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.4: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + openapi-types@12.1.3: {} oxfmt@0.17.0: @@ -1911,8 +3134,13 @@ snapshots: pathe@2.0.3: {} + perfect-debounce@2.0.0: {} + picocolors@1.1.1: {} + picomatch@2.3.1: + optional: true + picomatch@4.0.3: {} postcss@8.5.6: @@ -1921,10 +3149,28 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + property-information@7.1.0: {} + quansync@1.0.0: {} + react@19.2.3: {} + + readdirp@4.1.2: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + resolve-pkg-maps@1.0.0: {} + rfdc@1.4.1: {} + rolldown-plugin-dts@0.18.3(rolldown@1.0.0-beta.53)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.5 @@ -1989,20 +3235,64 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 + sass@1.97.1: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + search-insights@2.17.3: {} + semver@7.7.3: {} + shiki@3.20.0: + dependencies: + '@shikijs/core': 3.20.0 + '@shikijs/engine-javascript': 3.20.0 + '@shikijs/engine-oniguruma': 3.20.0 + '@shikijs/langs': 3.20.0 + '@shikijs/themes': 3.20.0 + '@shikijs/types': 3.20.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + siginfo@2.0.0: {} source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + stackback@0.0.2: {} std-env@3.10.0: {} + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 + swr@2.3.8(react@19.2.3): + dependencies: + dequal: 2.0.3 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + + tabbable@6.3.0: {} + + throttleit@2.1.0: {} + tinybench@2.9.0: {} tinyexec@1.0.2: {} @@ -2014,8 +3304,15 @@ snapshots: tinyrainbow@3.0.3: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + optional: true + tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + tsdown@0.17.3(typescript@5.9.3): dependencies: ansis: 4.2.0 @@ -2060,17 +3357,54 @@ snapshots: undici-types@7.16.0: {} + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + unrun@0.2.19: dependencies: rolldown: 1.0.0-beta.53 urlpattern-polyfill@10.1.0: {} + use-sync-external-store@1.6.0(react@19.2.3): + dependencies: + react: 19.2.3 + valibot@1.2.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 - vite@7.2.7(@types/node@25.0.2)(tsx@4.21.0): + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@7.2.7(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -2081,12 +3415,65 @@ snapshots: optionalDependencies: '@types/node': 25.0.2 fsevents: 2.3.3 + sass: 1.97.1 tsx: 4.21.0 - vitest@4.0.15(@types/node@25.0.2)(tsx@4.21.0): + vitepress@2.0.0-alpha.15(@algolia/client-search@5.46.2)(@types/node@25.0.2)(postcss@8.5.6)(react@19.2.3)(sass@1.97.1)(search-insights@2.17.3)(tsx@4.21.0)(typescript@5.9.3): + dependencies: + '@docsearch/css': 4.4.0 + '@docsearch/js': 4.4.0(@algolia/client-search@5.46.2)(react@19.2.3)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.64 + '@shikijs/core': 3.20.0 + '@shikijs/transformers': 3.20.0 + '@shikijs/types': 3.20.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 6.0.3(vite@7.2.7(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0))(vue@3.5.26(typescript@5.9.3)) + '@vue/devtools-api': 8.0.5 + '@vue/shared': 3.5.26 + '@vueuse/core': 14.1.0(vue@3.5.26(typescript@5.9.3)) + '@vueuse/integrations': 14.1.0(focus-trap@7.7.0)(vue@3.5.26(typescript@5.9.3)) + focus-trap: 7.7.0 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 3.20.0 + vite: 7.2.7(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0) + vue: 3.5.26(typescript@5.9.3) + optionalDependencies: + postcss: 8.5.6 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jiti + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - tsx + - typescript + - universal-cookie + - yaml + + vitest@4.0.15(@opentelemetry/api@1.9.0)(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0): dependencies: '@vitest/expect': 4.0.15 - '@vitest/mocker': 4.0.15(vite@7.2.7(@types/node@25.0.2)(tsx@4.21.0)) + '@vitest/mocker': 4.0.15(vite@7.2.7(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0)) '@vitest/pretty-format': 4.0.15 '@vitest/runner': 4.0.15 '@vitest/snapshot': 4.0.15 @@ -2103,9 +3490,10 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.2.7(@types/node@25.0.2)(tsx@4.21.0) + vite: 7.2.7(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 25.0.2 transitivePeerDependencies: - jiti @@ -2120,6 +3508,16 @@ snapshots: - tsx - yaml + vue@3.5.26(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-sfc': 3.5.26 + '@vue/runtime-dom': 3.5.26 + '@vue/server-renderer': 3.5.26(vue@3.5.26(typescript@5.9.3)) + '@vue/shared': 3.5.26 + optionalDependencies: + typescript: 5.9.3 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -2130,3 +3528,5 @@ snapshots: zod: 4.1.13 zod@4.1.13: {} + + zwitch@2.0.4: {} From 1d74224939ab02c9765caf33e84188b6dbb0f0ca Mon Sep 17 00:00:00 2001 From: robertpitt Date: Mon, 29 Dec 2025 16:17:35 +0000 Subject: [PATCH 2/2] Added documetnation site --- docs-site/.vitepress/config.ts | 136 ++- docs-site/api/create-contract.md | 208 ++++ docs-site/api/create-openapi-specification.md | 217 ++++ docs-site/api/create-router.md | 242 +++++ docs-site/api/index.md | 38 +- docs-site/api/middleware-api.md | 319 ++++++ docs-site/api/types.md | 315 ++++++ docs-site/examples/authentication.md | 147 +++ docs-site/examples/complex.md | 296 ++++++ docs-site/examples/content-types.md | 140 +++ docs-site/examples/file-upload.md | 155 +++ docs-site/examples/index.md | 78 +- docs-site/examples/simple.md | 230 ++++ docs-site/examples/valibot.md | 205 ++++ docs-site/guide/advanced-patterns.md | 446 ++++++++ docs-site/guide/best-practices.md | 508 +++++++++ docs-site/guide/content-types.md | 458 ++++++++ docs-site/guide/contracts.md | 503 +++++++++ docs-site/guide/core-concepts.md | 276 ++++- docs-site/guide/error-handling.md | 423 ++++++++ docs-site/guide/faq.md | 373 +++++++ docs-site/guide/getting-started.md | 309 ++++++ docs-site/guide/middleware.md | 462 ++++++++ docs-site/guide/migration.md | 328 ++++++ docs-site/guide/openapi.md | 452 ++++++++ docs-site/guide/router-configuration.md | 416 ++++++++ docs-site/guide/schema-libraries.md | 387 +++++++ docs-site/guide/troubleshooting.md | 339 ++++++ docs-site/guide/type-safety.md | 466 +++++++++ docs-site/guide/validation.md | 432 ++++++++ package.json | 9 +- pnpm-lock.yaml | 982 ++++++++++++++++++ 32 files changed, 10178 insertions(+), 117 deletions(-) create mode 100644 docs-site/api/create-contract.md create mode 100644 docs-site/api/create-openapi-specification.md create mode 100644 docs-site/api/create-router.md create mode 100644 docs-site/api/middleware-api.md create mode 100644 docs-site/api/types.md create mode 100644 docs-site/examples/authentication.md create mode 100644 docs-site/examples/complex.md create mode 100644 docs-site/examples/content-types.md create mode 100644 docs-site/examples/file-upload.md create mode 100644 docs-site/examples/simple.md create mode 100644 docs-site/examples/valibot.md create mode 100644 docs-site/guide/advanced-patterns.md create mode 100644 docs-site/guide/best-practices.md create mode 100644 docs-site/guide/content-types.md create mode 100644 docs-site/guide/contracts.md create mode 100644 docs-site/guide/error-handling.md create mode 100644 docs-site/guide/faq.md create mode 100644 docs-site/guide/middleware.md create mode 100644 docs-site/guide/migration.md create mode 100644 docs-site/guide/openapi.md create mode 100644 docs-site/guide/router-configuration.md create mode 100644 docs-site/guide/schema-libraries.md create mode 100644 docs-site/guide/troubleshooting.md create mode 100644 docs-site/guide/type-safety.md create mode 100644 docs-site/guide/validation.md diff --git a/docs-site/.vitepress/config.ts b/docs-site/.vitepress/config.ts index df9115d..3340d0f 100644 --- a/docs-site/.vitepress/config.ts +++ b/docs-site/.vitepress/config.ts @@ -1,56 +1,98 @@ import { defineConfig } from 'vitepress'; +import { withMermaid } from 'vitepress-plugin-mermaid'; -export default defineConfig({ - title: 'itty-spec', - description: 'Contract-first, type-safe API definitions for itty-router', - base: '/itty-spec/', - lastUpdated: true, - ignoreDeadLinks: true, - cleanUrls: true, - markdown: { - theme: { - light: 'github-light', - dark: 'github-dark', +export default withMermaid( + defineConfig({ + title: 'itty-spec', + description: 'Contract-first, type-safe API definitions for itty-router', + base: '/itty-spec/', + lastUpdated: true, + ignoreDeadLinks: true, + cleanUrls: true, + markdown: { + theme: { + light: 'github-light', + dark: 'github-dark', + }, }, - }, - themeConfig: { - nav: [ - { text: 'Guide', link: '/guide/getting-started' }, - { text: 'API', link: '/api/' }, - { text: 'Examples', link: '/examples/' }, - ], - sidebar: { - '/guide/': [ - { - text: 'Getting Started', - items: [ - { text: 'Introduction', link: '/guide/getting-started' }, - { text: 'Core Concepts', link: '/guide/core-concepts' }, - ], - }, - ], - '/api/': [ - { - text: 'API Reference', - items: [{ text: 'Overview', link: '/api/' }], - }, + themeConfig: { + nav: [ + { text: 'Guide', link: '/guide/getting-started' }, + { text: 'API', link: '/api/' }, + { text: 'Examples', link: '/examples/' }, ], - '/examples/': [ + sidebar: { + '/guide/': [ + { + text: 'Getting Started', + items: [ + { text: 'Introduction', link: '/guide/getting-started' }, + { text: 'Core Concepts', link: '/guide/core-concepts' }, + ], + }, + { + text: 'Guides', + items: [ + { text: 'Contracts', link: '/guide/contracts' }, + { text: 'Router Configuration', link: '/guide/router-configuration' }, + { text: 'Validation', link: '/guide/validation' }, + { text: 'Type Safety', link: '/guide/type-safety' }, + { text: 'Content Types', link: '/guide/content-types' }, + { text: 'Middleware', link: '/guide/middleware' }, + { text: 'Error Handling', link: '/guide/error-handling' }, + { text: 'OpenAPI Integration', link: '/guide/openapi' }, + { text: 'Schema Libraries', link: '/guide/schema-libraries' }, + { text: 'Best Practices', link: '/guide/best-practices' }, + { text: 'Advanced Patterns', link: '/guide/advanced-patterns' }, + ], + }, + { + text: 'Additional', + items: [ + { text: 'Migration Guide', link: '/guide/migration' }, + { text: 'Troubleshooting', link: '/guide/troubleshooting' }, + { text: 'FAQ', link: '/guide/faq' }, + ], + }, + ], + '/api/': [ + { + text: 'API Reference', + items: [ + { text: 'Overview', link: '/api/' }, + { text: 'createContract', link: '/api/create-contract' }, + { text: 'createRouter', link: '/api/create-router' }, + { text: 'createOpenApiSpecification', link: '/api/create-openapi-specification' }, + { text: 'Types', link: '/api/types' }, + { text: 'Middleware API', link: '/api/middleware-api' }, + ], + }, + ], + '/examples/': [ + { + text: 'Examples', + items: [ + { text: 'Overview', link: '/examples/' }, + { text: 'Simple Example', link: '/examples/simple' }, + { text: 'Complex Example', link: '/examples/complex' }, + { text: 'Valibot Example', link: '/examples/valibot' }, + { text: 'Content Types', link: '/examples/content-types' }, + { text: 'Authentication', link: '/examples/authentication' }, + { text: 'File Upload', link: '/examples/file-upload' }, + ], + }, + ], + }, + socialLinks: [ { - text: 'Examples', - items: [{ text: 'Overview', link: '/examples/' }], + icon: 'github', + link: 'https://github.com/robertpitt/itty-spec', }, ], - }, - socialLinks: [ - { - icon: 'github', - link: 'https://github.com/robertpitt/itty-spec', + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2024', }, - ], - footer: { - message: 'Released under the MIT License.', - copyright: 'Copyright © 2024', }, - }, -}); + }) +); diff --git a/docs-site/api/create-contract.md b/docs-site/api/create-contract.md new file mode 100644 index 0000000..c561be3 --- /dev/null +++ b/docs-site/api/create-contract.md @@ -0,0 +1,208 @@ +# createContract + +Creates a contract object that defines your API operations. + +## Signature + +```ts +function createContract( + definition: T +): T +``` + +## Type Parameters + +- `T` - The contract definition type (must extend `ContractDefinition`) + +## Parameters + +- `definition` - An object mapping operation IDs to contract operations + +## Returns + +The same contract definition with full type inference preserved. + +## Example + +```ts +import { createContract } from "itty-spec"; +import { z } from "zod"; + +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ + id: z.string().uuid(), + }), + responses: { + 200: { + "application/json": { + body: z.object({ + id: z.string().uuid(), + name: z.string(), + email: z.string().email(), + }), + }, + }, + }, + }, +}); +``` + +## Contract Operation Structure + +Each operation in a contract can include: + +### Required Fields + +- `path` - Route pattern (e.g., `"/users/:id"`) +- `method` - HTTP method (`"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"`) +- `responses` - Response schemas keyed by status code and content type + +### Optional Fields + +- `operationId` - Unique identifier for the operation +- `summary` - Short description +- `description` - Detailed description (supports markdown) +- `title` - Display title +- `tags` - Array of tags for grouping +- `pathParams` - Schema for path parameters +- `query` - Schema for query parameters +- `headers` - Schema for request headers +- `requests` - Request body schemas keyed by content type + +## Type Inference Tips + +### Use `as const` for Path Parameters + +For automatic path parameter extraction, use `as const`: + +```ts +// ✅ Good - path params are extracted +const contract = createContract({ + getUser: { + path: "/users/:id", // TypeScript infers { id: string } + method: "GET", + responses: { /* ... */ }, + }, +} as const); + +// ⚠️ May not extract path params +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + responses: { /* ... */ }, + }, +}); +``` + +### Explicit Path Parameter Schemas + +For validation beyond string types: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ + id: z.string().uuid(), // Validates UUID format + }), + responses: { /* ... */ }, + }, +}); +``` + +## Complete Example + +```ts +import { createContract } from "itty-spec"; +import { z } from "zod"; + +const UserSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + email: z.string().email(), +}); + +const CreateUserRequest = z.object({ + name: z.string().min(1), + email: z.string().email(), +}); + +const contract = createContract({ + getUser: { + operationId: "getUserById", + summary: "Get user by ID", + description: "Retrieves a user by their unique identifier", + tags: ["Users"], + path: "/users/:id", + method: "GET", + pathParams: z.object({ + id: z.string().uuid(), + }), + headers: z.object({ + authorization: z.string(), + }), + responses: { + 200: { + "application/json": { + body: UserSchema, + }, + }, + 404: { + "application/json": { + body: z.object({ + error: z.string(), + message: z.string(), + }), + }, + }, + }, + }, + createUser: { + operationId: "createUser", + summary: "Create a new user", + description: "Creates a new user account", + tags: ["Users"], + path: "/users", + method: "POST", + headers: z.object({ + authorization: z.string(), + "content-type": z.literal("application/json"), + }), + requests: { + "application/json": { + body: CreateUserRequest, + }, + }, + responses: { + 201: { + "application/json": { + body: UserSchema, + headers: z.object({ + location: z.string().url(), + }), + }, + }, + 400: { + "application/json": { + body: z.object({ + error: z.string(), + details: z.array(z.unknown()), + }), + }, + }, + }, + }, +} as const); +``` + +## Related + +- [Contracts Guide](/guide/contracts) - Learn about contract definitions +- [Type Safety](/guide/type-safety) - Understand type inference +- [createRouter](/api/create-router) - Create a router from a contract + diff --git a/docs-site/api/create-openapi-specification.md b/docs-site/api/create-openapi-specification.md new file mode 100644 index 0000000..f4a7c19 --- /dev/null +++ b/docs-site/api/create-openapi-specification.md @@ -0,0 +1,217 @@ +# createOpenApiSpecification + +Generates an OpenAPI 3.1 specification from a contract definition. + +## Signature + +```ts +function createOpenApiSpecification( + contract: ContractDefinition, + options: OpenApiSpecificationOptions +): Promise +``` + +## Parameters + +### contract + +**Type**: `ContractDefinition` + +**Required**: Yes + +The contract definition to generate OpenAPI spec from. + +### options + +**Type**: `OpenApiSpecificationOptions` + +**Required**: Yes + +Configuration options for the OpenAPI specification. + +## Options + +### options.title + +**Type**: `string` + +**Required**: Yes + +The title of the API. + +### options.version + +**Type**: `string` + +**Required**: No + +**Default**: `"0.0.0"` + +The version of the API. + +### options.description + +**Type**: `string` + +**Required**: No + +API description. Supports markdown. + +### options.summary + +**Type**: `string` + +**Required**: No + +Short summary of the API. + +### options.termsOfService + +**Type**: `string` + +**Required**: No + +URL to the terms of service. + +### options.contact + +**Type**: `{ name?: string; url?: string; email?: string }` + +**Required**: No + +Contact information for the API. + +### options.license + +**Type**: `{ identifier?: string; name?: string; url?: string }` + +**Required**: No + +License information for the API. + +### options.servers + +**Type**: `Array<{ url: string; description?: string }>` + +**Required**: No + +List of server URLs where the API is available. + +### options.tags + +**Type**: `Array<{ name: string; description?: string }>` + +**Required**: No + +Tags for grouping operations in the documentation. + +## Returns + +A Promise that resolves to an OpenAPI 3.1 Document. + +## Example + +```ts +import { createOpenApiSpecification } from "itty-spec/openapi"; +import { contract } from "./contract"; + +const openApiSpec = await createOpenApiSpecification(contract, { + title: "User Management API", + version: "1.0.0", + description: ` +# User Management API + +This API provides endpoints for managing users. + +## Features + +- User CRUD operations +- Authentication +- Role-based access control + `, + servers: [ + { url: "https://api.example.com", description: "Production" }, + { url: "https://staging-api.example.com", description: "Staging" }, + ], + contact: { + name: "API Support", + email: "support@example.com", + url: "https://example.com/support", + }, + license: { + identifier: "MIT", + name: "MIT License", + url: "https://opensource.org/licenses/MIT", + }, + termsOfService: "https://example.com/terms", + tags: [ + { name: "Users", description: "User management endpoints" }, + { name: "Authentication", description: "Authentication endpoints" }, + ], +}); +``` + +## Schema Deduplication + +The function automatically deduplicates schemas. If the same schema is used multiple times, it's defined once in `components.schemas` and referenced elsewhere via `$ref`. + +## Schema Registry + +Schemas are registered in a `SchemaRegistry` that: +- Deduplicates identical schemas +- Creates references for reused schemas +- Handles empty schemas +- Manages reference-only schemas + +## Generated Structure + +The generated OpenAPI spec includes: + +- `openapi: "3.1.1"` - OpenAPI version +- `info` - API information from options +- `servers` - Server URLs from options +- `paths` - Paths generated from contract operations +- `components.schemas` - Reusable schema definitions + +## Serving the Spec + +Add the spec as a route in your router: + +```ts +import { createRouter } from "itty-spec"; +import { z } from "zod"; + +const openApiSpec = await createOpenApiSpecification(contract, options); + +const router = createRouter({ + contract: { + ...contract, + getOpenApiSpec: { + path: "/openapi.json", + method: "GET", + responses: { + 200: { + "application/json": { body: z.any() }, + }, + }, + }, + }, + handlers: { + ...yourHandlers, + getOpenApiSpec: async (request) => { + return request.respond({ + status: 200, + contentType: "application/json", + body: openApiSpec, + }); + }, + }, +}); +``` + +## Related + +- [OpenAPI Integration Guide](/guide/openapi) - Learn about OpenAPI integration +- [createContract](/api/create-contract) - Create a contract +- [createRouter](/api/create-router) - Create a router + diff --git a/docs-site/api/create-router.md b/docs-site/api/create-router.md new file mode 100644 index 0000000..17cee94 --- /dev/null +++ b/docs-site/api/create-router.md @@ -0,0 +1,242 @@ +# createRouter + +Creates a type-safe router from a contract definition with automatic route registration and validation. + +## Signature + +```ts +function createRouter< + TContract extends ContractDefinition, + RequestType extends IRequest = IRequest, + Args extends any[] = any[] +>( + options: ContractRouterOptions +): RouterType +``` + +## Type Parameters + +- `TContract` - The contract definition type +- `RequestType` - The request type (default: `IRequest`) +- `Args` - Additional arguments passed to handlers (default: `any[]`) + +## Parameters + +### options.contract + +**Type**: `TContract` + +**Required**: Yes + +The contract definition mapping operation IDs to operations. + +### options.handlers + +**Type**: `{ [K in keyof TContract]?: ContractOperationHandler }` + +**Required**: Yes + +Handlers for each operation in the contract. Each handler receives a typed request object. + +### options.base + +**Type**: `string | undefined` + +**Required**: No + +Base path to prefix all routes. Useful for API versioning or mounting routers. + +### options.missing + +**Type**: `(request: RequestType & { respond: ... }, ...args: Args) => Response | Promise` + +**Required**: No + +Handler for unmatched routes. Defaults to returning a 404 response. + +### options.before + +**Type**: `RequestHandler[]` + +**Required**: No + +Middleware to run before handlers. Runs after built-in validation middleware. + +### options.finally + +**Type**: `ResponseHandler[]` + +**Required**: No + +Middleware to run after handlers. Runs before response formatting. + +### options.format + +**Type**: `ResponseHandler` + +**Required**: No + +Custom response formatter. Defaults to contract-aware JSON formatter. + +## Returns + +An itty-router instance with registered routes and middleware. + +## Example + +```ts +import { createRouter } from "itty-spec"; +import { contract } from "./contract"; + +const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { + const { id } = request.validatedParams; + const user = await getUserById(id); + + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); + }, + createUser: async (request) => { + const body = request.validatedBody; + const user = await createUser(body); + + return request.respond({ + status: 201, + contentType: "application/json", + body: user, + }); + }, + }, +}); +``` + +## Advanced Usage + +### Custom Request Type + +```ts +interface AuthenticatedRequest extends IRequest { + user: User; +} + +const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { + // request.user is available + const user = request.user; + // ... + }, + }, +}); +``` + +### Additional Handler Arguments + +```ts +type Context = { + db: Database; + logger: Logger; +}; + +const router = createRouter({ + contract, + handlers: { + getUser: async (request, context) => { + // context is typed as Context + context.logger.info("Fetching user"); + const user = await context.db.findUser(request.validatedParams.id); + // ... + }, + }, +}); + +// Pass context when calling +router.fetch(request, { db, logger }); +``` + +### Base Path + +```ts +const router = createRouter({ + contract, + handlers, + base: "/api/v1", // All routes prefixed with /api/v1 +}); + +// Contract path: "/users" +// Actual route: "/api/v1/users" +``` + +### Custom Middleware + +```ts +const router = createRouter({ + contract, + handlers, + before: [ + async (request) => { + console.log(`${request.method} ${request.url}`); + }, + async (request) => { + const auth = request.headers.get("authorization"); + if (!auth) { + throw new Error("Unauthorized"); + } + }, + ], + finally: [ + async (response, request) => { + response.headers.set("access-control-allow-origin", "*"); + return response; + }, + ], +}); +``` + +### Custom Missing Handler + +```ts +const router = createRouter({ + contract, + handlers, + missing: async (request) => { + return request.respond({ + status: 404, + contentType: "application/json", + body: { + error: "Not Found", + path: new URL(request.url).pathname, + }, + }); + }, +}); +``` + +## Built-in Middleware + +The router automatically includes these middleware: + +### Before Middleware (in order) + +1. `withParams` - Extracts path parameters +2. `withMatchingContractOperation` - Finds matching operation +3. `withSpecValidation` - Validates request data +4. `withResponseHelpers` - Attaches `respond()` method + +### Finally Middleware (in order) + +1. `withMissingHandler` - Handles 404s +2. `withContractFormat` - Formats responses + +## Related + +- [Router Configuration Guide](/guide/router-configuration) - Learn about router options +- [Middleware Guide](/guide/middleware) - Understand middleware +- [createContract](/api/create-contract) - Create a contract + diff --git a/docs-site/api/index.md b/docs-site/api/index.md index e89359a..893812b 100644 --- a/docs-site/api/index.md +++ b/docs-site/api/index.md @@ -1,6 +1,10 @@ # API Reference -## createContract +Complete API reference for `itty-spec`. All functions, types, and utilities are documented here. + +## Core Functions + +### [createContract](/api/create-contract) Creates a contract object that defines your API operations. @@ -10,13 +14,13 @@ import { createContract } from "itty-spec"; const contract = createContract({ operationName: { path: "/path", - method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", - // ... operation definition + method: "GET", + responses: { /* ... */ }, }, }); ``` -## createRouter +### [createRouter](/api/create-router) Creates a router with handlers bound to a contract. @@ -33,7 +37,7 @@ const router = createRouter({ }); ``` -## createOpenApiSpecification +### [createOpenApiSpecification](/api/create-openapi-specification) Generates an OpenAPI 3.1 specification from a contract. @@ -48,5 +52,27 @@ const spec = await createOpenApiSpecification(contract, { }); ``` -For detailed API documentation, refer to the TypeScript definitions in the source code. +## Type Reference + +### [Types](/api/types) + +Complete reference for all TypeScript types used in `itty-spec`: + +- Contract types +- Request types +- Response types +- Helper types + +### [Middleware API](/api/middleware-api) + +Reference for built-in middleware and custom middleware patterns. + +## Quick Links + +- [Contracts Guide](/guide/contracts) - Learn about contract definitions +- [Router Configuration](/guide/router-configuration) - Configure your router +- [Type Safety](/guide/type-safety) - Understand type inference +- [Examples](/examples/) - Working examples + +For detailed type definitions, see the TypeScript source code. diff --git a/docs-site/api/middleware-api.md b/docs-site/api/middleware-api.md new file mode 100644 index 0000000..7b0da63 --- /dev/null +++ b/docs-site/api/middleware-api.md @@ -0,0 +1,319 @@ +# Middleware API + +Reference for built-in middleware and custom middleware patterns in `itty-spec`. + +## Built-in Middleware + +### withMatchingContractOperation + +Finds and sets the matching contract operation from a contract. + +**Type**: `RequestHandler` + +**Usage**: Automatically included in router's `before` array. + +**Purpose**: Matches incoming requests to contract operations based on method and path pattern. + +```ts +import { withMatchingContractOperation } from "itty-spec/middleware"; + +// Automatically included, but can be used manually: +const middleware = withMatchingContractOperation(contract, basePath); +``` + +### withSpecValidation + +Validates path params, query params, headers, and body against contract schemas. + +**Type**: `RequestHandler` + +**Usage**: Automatically included in router's `before` array. + +**Purpose**: Validates all request data before handlers run. + +```ts +import { withSpecValidation } from "itty-spec/middleware"; + +// Automatically included +``` + +### withResponseHelpers + +Attaches typed `respond()` method to request object. + +**Type**: `RequestHandler` + +**Usage**: Automatically included in router's `before` array. + +**Purpose**: Provides type-safe response helper method. + +```ts +import { withResponseHelpers } from "itty-spec/middleware"; + +// Automatically included +// Makes request.respond() available in handlers +``` + +### withContractFormat + +Formats contract response objects to Response objects. + +**Type**: `ResponseHandler` + +**Usage**: Automatically included in router's `finally` array. + +**Purpose**: Converts contract response format to HTTP Response. + +```ts +import { withContractFormat } from "itty-spec/middleware"; + +// Automatically included +// Can be customized: +const router = createRouter({ + contract, + handlers, + format: withContractFormat(customFormatter), +}); +``` + +### withContractErrorHandler + +Handles validation errors and other errors. + +**Type**: `(err: unknown, request: RequestType, ...args: Args) => Response` + +**Usage**: Automatically set as router's `catch` handler. + +**Purpose**: Converts errors to appropriate HTTP responses. + +```ts +import { withContractErrorHandler } from "itty-spec/middleware"; + +// Automatically included +// Can be customized: +const router = Router({ + catch: withContractErrorHandler(), +}); +``` + +### withMissingHandler + +Handles missing routes (404s). + +**Type**: `ResponseHandler` + +**Usage**: Automatically included in router's `finally` array. + +**Purpose**: Returns 404 for unmatched routes. + +```ts +import { withMissingHandler } from "itty-spec/middleware"; + +// Automatically included +// Can be customized: +const router = createRouter({ + contract, + handlers, + missing: async (request) => { + return request.respond({ + status: 404, + contentType: "application/json", + body: { error: "Not Found" }, + }); + }, +}); +``` + +## Middleware Types + +### RequestHandler + +Type for before middleware. + +```ts +type RequestHandler< + RequestType extends IRequest = IRequest, + Args extends any[] = any[] +> = ( + request: RequestType, + ...args: Args +) => void | Promise | Response | Promise +``` + +If a middleware returns a `Response`, it short-circuits the request. + +### ResponseHandler + +Type for finally middleware. + +```ts +type ResponseHandler = ( + response: unknown, + request: IRequest +) => Response | Promise +``` + +## Custom Middleware Patterns + +### Authentication Middleware + +```ts +async function withAuth(request: IRequest) { + const authHeader = request.headers.get("authorization"); + + if (!authHeader) { + throw new Error("Unauthorized"); + } + + const user = await verifyToken(authHeader); + (request as any).user = user; +} + +const router = createRouter({ + contract, + handlers, + before: [withAuth], +}); +``` + +### Logging Middleware + +```ts +async function withLogging(request: IRequest) { + const startTime = Date.now(); + (request as any).startTime = startTime; + + console.log(`${request.method} ${request.url}`); +} + +const router = createRouter({ + contract, + handlers, + before: [withLogging], + finally: [ + async (response, request) => { + const startTime = (request as any).startTime; + if (startTime) { + const duration = Date.now() - startTime; + console.log(`Duration: ${duration}ms`); + } + return response; + }, + ], +}); +``` + +### CORS Middleware + +```ts +function withCORS(allowedOrigins: string[] = ["*"]) { + return async (response: Response, request: IRequest) => { + const origin = request.headers.get("origin"); + + if (allowedOrigins.includes("*") || (origin && allowedOrigins.includes(origin))) { + response.headers.set("access-control-allow-origin", origin || "*"); + response.headers.set("access-control-allow-methods", "GET, POST, PUT, DELETE"); + response.headers.set("access-control-allow-headers", "Content-Type, Authorization"); + } + + return response; + }; +} + +const router = createRouter({ + contract, + handlers, + finally: [withCORS(["https://example.com"])], +}); +``` + +### Rate Limiting Middleware + +```ts +const rateLimiter = new Map(); + +function withRateLimit(maxRequests: number = 100, windowMs: number = 60000) { + return async (request: IRequest) => { + const key = request.headers.get("x-forwarded-for") || "unknown"; + const now = Date.now(); + + const limit = rateLimiter.get(key); + + if (limit && limit.resetAt > now) { + if (limit.count >= maxRequests) { + throw new Error("Rate limit exceeded"); + } + limit.count++; + } else { + rateLimiter.set(key, { count: 1, resetAt: now + windowMs }); + } + }; +} + +const router = createRouter({ + contract, + handlers, + before: [withRateLimit(100, 60000)], +}); +``` + +## Middleware Ordering + +Middleware runs in the order specified: + +```ts +const router = createRouter({ + contract, + handlers, + before: [ + middleware1, // Runs first + middleware2, // Runs second + middleware3, // Runs third + ], + finally: [ + middleware4, // Runs first (after handler) + middleware5, // Runs second + ], +}); +``` + +### Built-in Middleware Order + +Built-in middleware always runs in a specific order: + +**Before:** +1. `withParams` +2. `withMatchingContractOperation` +3. `withSpecValidation` +4. `withResponseHelpers` +5. Your custom `before` middleware + +**Finally:** +1. `withMissingHandler` +2. `withContractFormat` +3. Your custom `finally` middleware + +## Short-Circuiting + +If a before middleware returns a `Response`, it short-circuits the request: + +```ts +async function withAuth(request: IRequest) { + const auth = request.headers.get("authorization"); + if (!auth) { + // Short-circuit: return error immediately + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { status: 401 } + ); + } + // Continue to next middleware/handler +} +``` + +## Related + +- [Middleware Guide](/guide/middleware) - Learn about middleware patterns +- [Router Configuration](/guide/router-configuration) - Configure middleware +- [Error Handling](/guide/error-handling) - Handle errors in middleware + diff --git a/docs-site/api/types.md b/docs-site/api/types.md new file mode 100644 index 0000000..d8395e6 --- /dev/null +++ b/docs-site/api/types.md @@ -0,0 +1,315 @@ +# Types Reference + +Complete reference for all TypeScript types used in `itty-spec`. + +## Contract Types + +### ContractDefinition + +A record of operation IDs to contract operations. + +```ts +type ContractDefinition = Record> +``` + +### ContractOperation + +Defines a single API operation. + +```ts +interface ContractOperation< + TPathParams extends StandardSchemaV1 | undefined = undefined, + TQuery extends StandardSchemaV1 | undefined = undefined, + TRequests extends RequestByContentType | undefined = undefined, + THeaders extends StandardSchemaV1 | undefined = undefined, + TResponses extends ResponseByStatusCode = ResponseByStatusCode, + TPath extends string = string +> +``` + +### Contract + +Alias for contract definition type. + +```ts +type Contract = T +``` + +## Request Types + +### ContractRequest + +The request object passed to handlers, with typed validated data and response helpers. + +```ts +interface ContractRequest + extends ContractOperationRequest, ContractOperationResponseHelpers +``` + +### ContractOperationRequest + +Request object with typed validated data. + +```ts +interface ContractOperationRequest extends IRequest { + validatedParams: ContractOperationParameters; + validatedQuery: ContractOperationQuery; + validatedBody: ContractOperationBody; + validatedHeaders: ContractOperationHeaders; +} +``` + +### ContractOperationParameters + +Extract path parameter types from an operation. + +```ts +type ContractOperationParameters +``` + +### ContractOperationQuery + +Extract query parameter types from an operation. + +```ts +type ContractOperationQuery +``` + +### ContractOperationBody + +Extract body types from an operation. + +```ts +type ContractOperationBody +``` + +### ContractOperationHeaders + +Extract header types from an operation. + +```ts +type ContractOperationHeaders +``` + +## Response Types + +### ContractOperationResponse + +Response type that must match one of the contract's response schemas. + +```ts +type ContractOperationResponse +``` + +### ContractOperationStatusCodes + +Extract valid status codes from an operation. + +```ts +type ContractOperationStatusCodes +``` + +### ContractOperationResponseBody + +Extract body type for a specific status code and content type. + +```ts +type ContractOperationResponseBody< + O extends ContractOperation, + S extends ContractOperationStatusCodes, + C extends string = 'application/json' +> +``` + +### ContractOperationResponseHeaders + +Extract headers type for a specific status code and content type. + +```ts +type ContractOperationResponseHeaders< + O extends ContractOperation, + S extends ContractOperationStatusCodes, + C extends string = 'application/json' +> +``` + +### ResponseVariant + +Extract a specific response variant from the union by status code. + +```ts +type ResponseVariant< + O extends ContractOperation, + S extends ContractOperationStatusCodes +> +``` + +## Helper Types + +### ExtractPathParams + +Extract path parameters from a path string. + +```ts +type ExtractPathParams +``` + +Example: +```ts +type Params = ExtractPathParams<"/users/:id/posts/:postId">; +// Type: { id: string; postId: string } +``` + +### ExtractContentTypes + +Extract all valid content types for a given status code. + +```ts +type ExtractContentTypes< + O extends ContractOperation, + S extends ContractOperationStatusCodes +> +``` + +### RespondOptions + +Options for the `respond()` method. + +```ts +type RespondOptions< + O extends ContractOperation, + S extends ContractOperationStatusCodes, + C extends ExtractContentTypes & string +> +``` + +## Router Types + +### ContractRouterOptions + +Options for `createRouter`. + +```ts +interface ContractRouterOptions< + TContract extends ContractDefinition, + RequestType extends IRequest = IRequest, + Args extends any[] = any[] +> +``` + +### ContractOperationHandler + +Handler function type for a contract operation. + +```ts +type ContractOperationHandler< + O extends ContractOperation, + Args extends any[] = any[] +> = ( + request: ContractRequest, + ...args: Args +) => Promise> +``` + +## Schema Types + +### ResponseSchema + +Response schema structure with body and optional headers. + +```ts +interface ResponseSchema< + TBody extends StandardSchemaV1 = StandardSchemaV1, + THeaders extends StandardSchemaV1 = StandardSchemaV1 +> +``` + +### ResponseByContentType + +Response schemas mapped by content type. + +```ts +type ResponseByContentType = { + [contentType: string]: ResponseSchema; +} +``` + +### RequestByContentType + +Request schemas mapped by content type. + +```ts +type RequestByContentType = { + [contentType: string]: { body: StandardSchemaV1 }; +} +``` + +## Utility Types + +### EmptyObject + +Canonical "empty object" type. + +```ts +type EmptyObject = Record +``` + +### RawQuery + +Default query object type when no schema is provided. + +```ts +type RawQuery = Record +``` + +### TypedHeaders + +Typed Headers interface that extends the standard Headers API. + +```ts +type TypedHeaders +``` + +## Usage Examples + +### Extract Types from Contract + +```ts +import type { + ContractOperationParameters, + ContractOperationQuery, + ContractOperationBody, +} from "itty-spec"; + +type Params = ContractOperationParameters; +type Query = ContractOperationQuery; +type Body = ContractOperationBody; +``` + +### Type Handler Functions + +```ts +import type { ContractOperationHandler } from "itty-spec"; + +const handler: ContractOperationHandler = async (request) => { + // request is fully typed + const { id } = request.validatedParams; + // ... +}; +``` + +### Extract Response Types + +```ts +import type { ContractOperationResponse } from "itty-spec"; + +type Response = ContractOperationResponse; +// Type: { status: 200; body: User } | { status: 404; body: Error } +``` + +## Related + +- [Type Safety Guide](/guide/type-safety) - Learn about type inference +- [Contracts Guide](/guide/contracts) - Understand contract types +- [API Overview](/api/) - See all API functions + diff --git a/docs-site/examples/authentication.md b/docs-site/examples/authentication.md new file mode 100644 index 0000000..5e3afd5 --- /dev/null +++ b/docs-site/examples/authentication.md @@ -0,0 +1,147 @@ +# Authentication Example + +Demonstrates authentication middleware patterns. + +## Overview + +This example shows: +- Authentication middleware +- Protected routes +- Token validation +- User context in handlers + +## Authentication Middleware + +```ts +// middleware/auth.middleware.ts +export interface AuthenticatedRequest extends IRequest { + userId?: string; + userRole?: string; +} + +export function withAuth(request: AuthenticatedRequest): void { + const authHeader = request.headers.get("authorization"); + const userId = extractUserIdFromAuth(authHeader); + + if (!userId) { + return; // Let handler decide what to do + } + + const user = userDb.findById(userId); + if (user) { + request.userId = user.id; + request.userRole = user.role; + } +} +``` + +## Contract with Authentication + +```ts +const contract = createContract({ + getProfile: { + path: "/profile", + method: "GET", + headers: z.object({ + authorization: z.string(), + }), + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + 401: { + "application/json": { body: ErrorSchema }, + }, + }, + }, +}); +``` + +## Protected Handler + +```ts +const handler = async (request: AuthenticatedRequest) => { + if (!request.userId) { + return request.respond({ + status: 401, + contentType: "application/json", + body: { + error: "Unauthorized", + message: "Authentication required", + }, + }); + } + + const user = await getUser(request.userId); + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); +}; +``` + +## Role-Based Access + +```ts +const handler = async (request: AuthenticatedRequest) => { + if (!request.userId) { + return request.respond({ + status: 401, + contentType: "application/json", + body: { error: "Unauthorized" }, + }); + } + + if (request.userRole !== "admin") { + return request.respond({ + status: 403, + contentType: "application/json", + body: { + error: "Forbidden", + message: "Admin access required", + }, + }); + } + + // Admin-only logic + const users = await getAllUsers(); + return request.respond({ + status: 200, + contentType: "application/json", + body: { users }, + }); +}; +``` + +## Router Setup + +```ts +const router = createRouter({ + contract, + handlers, + before: [withAuth], +}); +``` + +## Testing + +### Authenticated Request + +```bash +curl "http://localhost:3000/profile" \ + -H "Authorization: Bearer token123" +``` + +### Unauthenticated Request + +```bash +curl "http://localhost:3000/profile" +# Returns 401 Unauthorized +``` + +## Related + +- [Middleware Guide](/guide/middleware) - Learn about middleware +- [Complex Example](/examples/complex) - See authentication in a full example + diff --git a/docs-site/examples/complex.md b/docs-site/examples/complex.md new file mode 100644 index 0000000..51ee8e9 --- /dev/null +++ b/docs-site/examples/complex.md @@ -0,0 +1,296 @@ +# Complex Example + +A comprehensive example demonstrating advanced patterns and best practices. + +## Overview + +This example showcases a production-ready API with: +- Multi-domain contract organization +- Authentication middleware +- Pagination and filtering +- Database integration patterns +- Comprehensive error handling +- OpenAPI documentation + +## Project Structure + +``` +complex/ +├── contracts/ # Contract definitions by domain +│ ├── users.contract.ts +│ ├── products.contract.ts +│ ├── orders.contract.ts +│ └── index.ts +├── schemas/ # Reusable schemas +│ ├── common.ts +│ ├── users.ts +│ ├── products.ts +│ └── orders.ts +├── handlers/ # Request handlers by domain +│ ├── users.handlers.ts +│ ├── products.handlers.ts +│ └── orders.handlers.ts +├── middleware/ # Custom middleware +│ └── auth.middleware.ts +├── utils/ # Utilities +│ ├── database.ts +│ ├── pagination.ts +│ ├── auth.ts +│ └── docs.ts +└── index.ts # Main entry point +``` + +## Domain Organization + +### Contracts by Domain + +Each domain has its own contract file: + +```ts +// contracts/users.contract.ts +export const usersContract = createContract({ + getUsers: { /* ... */ }, + getUserById: { /* ... */ }, + createUser: { /* ... */ }, + updateUser: { /* ... */ }, + deleteUser: { /* ... */ }, +}); + +// contracts/index.ts +export const contract = { + ...usersContract, + ...productsContract, + ...ordersContract, +}; +``` + +### Schema Reuse + +Schemas are defined once and reused: + +```ts +// schemas/common.ts +export const PaginationQuery = z.object({ + page: z.number().min(1).default(1), + limit: z.number().min(1).max(100).default(10), +}); + +export const ErrorResponse = z.object({ + error: z.string(), + message: z.string(), +}); + +// Used in contracts +import { PaginationQuery, ErrorResponse } from "./schemas/common"; +``` + +## Authentication Middleware + +The example includes authentication middleware: + +```ts +// middleware/auth.middleware.ts +export function withAuth(request: AuthenticatedRequest): void { + const authHeader = request.headers.get("authorization"); + const userId = extractUserIdFromAuth(authHeader); + + if (!userId) { + return; // Let handler decide + } + + const user = userDb.findById(userId); + if (user) { + request.userId = user.id; + request.userRole = user.role; + } +} + +// Applied to all routes +const router = createRouter({ + contract, + before: [withAuth], + handlers: { /* ... */ }, +}); +``` + +## Pagination + +The example includes pagination utilities: + +```ts +// utils/pagination.ts +export function paginate( + items: T[], + page: number, + limit: number +): PaginatedResponse { + const start = (page - 1) * limit; + const end = start + limit; + + return { + data: items.slice(start, end), + meta: { + page, + limit, + total: items.length, + totalPages: Math.ceil(items.length / limit), + }, + }; +} + +// Used in handlers +const handler = async (request) => { + const { page, limit } = request.validatedQuery; + const users = await getAllUsers(); + const paginated = paginate(users, page, limit); + + return request.respond({ + status: 200, + contentType: "application/json", + body: paginated, + }); +}; +``` + +## Error Handling + +Comprehensive error handling patterns: + +```ts +const handler = async (request) => { + const user = await getUser(request.validatedParams.id); + + if (!user) { + return request.respond({ + status: 404, + contentType: "application/json", + body: { + error: "Not Found", + message: "User not found", + }, + }); + } + + // Check permissions + if (request.userRole !== "admin" && request.userId !== user.id) { + return request.respond({ + status: 403, + contentType: "application/json", + body: { + error: "Forbidden", + message: "You don't have permission to access this resource", + }, + }); + } + + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); +}; +``` + +## OpenAPI Integration + +The example generates and serves OpenAPI documentation: + +```ts +const openApiSpec = await createOpenApiSpecification(contract, { + title: "Complex API", + version: "1.0.0", + description: readFileSync(join(import.meta.dirname, "description.md"), "utf8"), + servers: [{ url: "http://localhost:3000", description: "Localhost" }], + tags: [ + { name: "Users", description: "User management endpoints" }, + { name: "Products", description: "Product management endpoints" }, + { name: "Orders", description: "Order management endpoints" }, + ], +}); + +// Serve OpenAPI spec and docs +const router = createRouter({ + contract: { + ...contract, + getSpec: { /* ... */ }, + getDocs: { /* ... */ }, + }, + handlers: { + ...handlers, + getSpec: async (request) => { + return request.respond({ + status: 200, + contentType: "application/json", + body: openApiSpec, + }); + }, + getDocs: async (request) => { + return request.respond({ + status: 200, + contentType: "text/html", + body: createSpotlightElementsHtml(), + }); + }, + }, +}); +``` + +## Key Patterns + +### 1. Domain Separation + +Each domain (users, products, orders) has: +- Its own contract file +- Its own handler file +- Its own schema file + +### 2. Schema Reuse + +Common schemas (pagination, errors) are defined once and reused. + +### 3. Middleware Composition + +Middleware is composed and reused across domains. + +### 4. Type Safety + +Full type inference from contracts to handlers. + +## Running the Example + +```bash +cd examples/complex +npm install +npm run dev +``` + +The server will start on `http://localhost:3000`. + +## API Endpoints + +- `GET /users` - List users with pagination +- `GET /users/:id` - Get user by ID +- `POST /users` - Create user +- `PATCH /users/:id` - Update user +- `DELETE /users/:id` - Delete user +- `GET /products` - List products +- `GET /orders` - List orders +- `GET /openapi.json` - OpenAPI specification +- `GET /docs` - Interactive API documentation + +## What to Learn + +This example demonstrates: +- Multi-domain organization +- Authentication patterns +- Pagination implementation +- Error handling strategies +- OpenAPI integration +- Production-ready patterns + +## Next Steps + +- Read the [Best Practices Guide](/guide/best-practices) +- Explore [Advanced Patterns](/guide/advanced-patterns) +- Check out [Middleware Guide](/guide/middleware) + diff --git a/docs-site/examples/content-types.md b/docs-site/examples/content-types.md new file mode 100644 index 0000000..ab0e741 --- /dev/null +++ b/docs-site/examples/content-types.md @@ -0,0 +1,140 @@ +# Content Types Example + +Demonstrates handling multiple content types and content negotiation. + +## Overview + +This example shows: +- Multiple request content types +- Multiple response content types +- Content negotiation based on Accept header +- HTML, XML, and JSON responses + +## Contract Definition + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + responses: { + 200: { + "application/json": { + body: z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + }), + }, + "text/html": { + body: z.string(), + }, + "application/xml": { + body: z.string(), + }, + }, + }, + }, + createUser: { + path: "/users", + method: "POST", + requests: { + "application/json": { + body: z.object({ + name: z.string(), + email: z.string().email(), + }), + }, + "application/x-www-form-urlencoded": { + body: z.object({ + name: z.string(), + email: z.string().email(), + }), + }, + }, + responses: { + 201: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); +``` + +## Content Negotiation + +The handler checks the `Accept` header to determine the response format: + +```ts +const handler = async (request) => { + const user = await getUser(request.validatedParams.id); + const accept = request.headers.get("accept") || "application/json"; + + if (accept.includes("text/html")) { + return request.respond({ + status: 200, + contentType: "text/html", + body: ` + + +

User Profile

+

Name: ${user.name}

+

Email: ${user.email}

+ + + `, + }); + } + + if (accept.includes("application/xml")) { + return request.respond({ + status: 200, + contentType: "application/xml", + body: ` + + + ${user.id} + ${user.name} + ${user.email} + + `, + }); + } + + // Default to JSON + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); +}; +``` + +## Testing + +### JSON Response + +```bash +curl "http://localhost:3000/users/123" \ + -H "Accept: application/json" +``` + +### HTML Response + +```bash +curl "http://localhost:3000/users/123" \ + -H "Accept: text/html" +``` + +### XML Response + +```bash +curl "http://localhost:3000/users/123" \ + -H "Accept: application/xml" +``` + +## Related + +- [Content Types Guide](/guide/content-types) - Learn about content types +- [Simple Example](/examples/simple) - See content negotiation in action + diff --git a/docs-site/examples/file-upload.md b/docs-site/examples/file-upload.md new file mode 100644 index 0000000..d45c107 --- /dev/null +++ b/docs-site/examples/file-upload.md @@ -0,0 +1,155 @@ +# File Upload Example + +Demonstrates handling file uploads with multipart form data. + +## Overview + +This example shows: +- Handling multipart form data +- File validation +- File storage patterns +- Response with file metadata + +## Contract Definition + +```ts +const contract = createContract({ + uploadFile: { + path: "/files", + method: "POST", + requests: { + "multipart/form-data": { + body: z.object({ + file: z.instanceof(File), + description: z.string().optional(), + }), + }, + }, + responses: { + 201: { + "application/json": { + body: z.object({ + id: z.string(), + filename: z.string(), + size: z.number(), + uploadedAt: z.string(), + }), + }, + }, + 400: { + "application/json": { + body: z.object({ error: z.string() }), + }, + }, + }, + }, +}); +``` + +## Handler Implementation + +```ts +const handler = async (request) => { + const formData = await request.formData(); + const file = formData.get("file") as File; + const description = formData.get("description") as string; + + // Validate file + if (!file) { + return request.respond({ + status: 400, + contentType: "application/json", + body: { error: "File is required" }, + }); + } + + // Validate file size + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + return request.respond({ + status: 400, + contentType: "application/json", + body: { error: "File too large. Maximum size is 10MB" }, + }); + } + + // Validate file type + const allowedTypes = ["image/jpeg", "image/png", "image/gif"]; + if (!allowedTypes.includes(file.type)) { + return request.respond({ + status: 400, + contentType: "application/json", + body: { error: "Invalid file type. Allowed types: JPEG, PNG, GIF" }, + }); + } + + // Save file + const fileId = await saveFile(file, description); + + return request.respond({ + status: 201, + contentType: "application/json", + body: { + id: fileId, + filename: file.name, + size: file.size, + uploadedAt: new Date().toISOString(), + }, + }); +}; +``` + +## File Storage + +```ts +async function saveFile(file: File, description?: string): Promise { + const fileId = crypto.randomUUID(); + const buffer = await file.arrayBuffer(); + + // Save to storage (implementation depends on environment) + // Cloudflare Workers: Use R2 + // Node.js: Use filesystem or S3 + // etc. + + await storage.put(fileId, buffer, { + metadata: { + filename: file.name, + contentType: file.type, + description, + }, + }); + + return fileId; +} +``` + +## Testing + +### Upload File + +```bash +curl -X POST "http://localhost:3000/files" \ + -F "file=@image.jpg" \ + -F "description=Profile picture" +``` + +### With JavaScript + +```ts +const formData = new FormData(); +formData.append("file", fileInput.files[0]); +formData.append("description", "Profile picture"); + +const response = await fetch("/files", { + method: "POST", + body: formData, +}); + +const result = await response.json(); +``` + +## Related + +- [Content Types Guide](/guide/content-types) - Learn about content types +- [Advanced Patterns](/guide/advanced-patterns) - More advanced patterns + diff --git a/docs-site/examples/index.md b/docs-site/examples/index.md index 55b2137..16e612e 100644 --- a/docs-site/examples/index.md +++ b/docs-site/examples/index.md @@ -1,35 +1,81 @@ # Examples -This project includes several example implementations to help you get started: +This project includes several example implementations to help you get started with `itty-spec`. -## Simple Example +## Getting Started -A basic example showing how to define a contract and create a router with handlers. +- **[Simple Example](/examples/simple)** - Basic contract and router setup +- **[Complex Example](/examples/complex)** - Multi-domain API with authentication -Location: `examples/simple/` +## Schema Libraries -## Complex Example +- **[Valibot Example](/examples/valibot)** - Using Valibot instead of Zod -A more comprehensive example demonstrating: +## Advanced Patterns + +- **[Content Types](/examples/content-types)** - Multiple content types and content negotiation +- **[Authentication](/examples/authentication)** - Authentication middleware patterns +- **[File Upload](/examples/file-upload)** - Handling file uploads + +## Example Features + +### Simple Example + +**Difficulty**: Beginner +**Features**: Basic contracts, handlers, content negotiation, OpenAPI + +A basic example showing: +- Contract definition +- Handler implementation +- Type inference +- Content negotiation +- OpenAPI generation + +[View Example →](/examples/simple) + +### Complex Example + +**Difficulty**: Intermediate +**Features**: Multi-domain, authentication, pagination, database patterns + +A comprehensive example demonstrating: - Multiple contracts organized by domain - Authentication middleware - Pagination utilities -- Database integration patterns -- OpenAPI specification generation +- Database integration +- Error handling patterns +- OpenAPI specification + +[View Example →](/examples/complex) + +### Valibot Example + +**Difficulty**: Beginner +**Features**: Valibot schemas, OpenAPI generation + +An example using Valibot instead of Zod: +- Valibot schema definitions +- Similar patterns to Zod +- OpenAPI generation support + +[View Example →](/examples/valibot) -Location: `examples/complex/` +## Running Examples -## Valibot Example +All examples can be run locally: -An example using Valibot instead of Zod for schema validation. +```bash +# Navigate to example directory +cd examples/simple -Location: `examples/valibot/` +# Install dependencies +npm install -## Repository Layout +# Run the example +npm run dev +``` -* `src/` - Library source code -* `examples/` - Usage examples -* `tests/` - Test suite +## Repository For complete working examples, check out the [examples directory](https://github.com/robertpitt/itty-spec/tree/main/examples) in the repository. diff --git a/docs-site/examples/simple.md b/docs-site/examples/simple.md new file mode 100644 index 0000000..f06bd7e --- /dev/null +++ b/docs-site/examples/simple.md @@ -0,0 +1,230 @@ +# Simple Example + +A basic example demonstrating the core features of `itty-spec`. + +## Overview + +This example shows: +- Defining a contract with multiple operations +- Creating handlers with typed request data +- Handling multiple content types +- Generating and serving OpenAPI documentation + +## Project Structure + +``` +simple/ +├── contract.ts # Contract definition +├── index.ts # Router and server setup +└── utils.ts # Utility functions +``` + +## Contract Definition + +The contract defines two operations: `getCalculate` (GET) and `postCalculate` (POST). + +```ts +import { createContract } from "itty-spec"; +import { z } from "zod"; + +export const contract = createContract({ + getCalculate: { + path: "/calculate", + method: "GET", + query: z.object({ + a: z.string().transform(Number).pipe(z.number().min(0).max(100)), + b: z.string().transform(Number).pipe(z.number().min(0).max(100)), + }), + headers: z.object({ + "Content-Type": z.union([ + z.literal("application/json"), + z.literal("text/html"), + z.literal("application/xml"), + ]), + }), + responses: { + 200: { + "application/json": { body: CalculateResponse }, + "text/html": { body: z.string() }, + "application/xml": { body: z.string() }, + }, + 400: { + "application/json": { body: CalculateError }, + "text/html": { body: z.string() }, + "application/xml": { body: z.string() }, + }, + }, + }, + postCalculate: { + path: "/calculate", + method: "POST", + requests: { + "application/json": { body: CalculatePostRequest }, + }, + responses: { + 200: { "application/json": { body: CalculateResponse } }, + 400: { "application/json": { body: CalculateError } }, + }, + }, +}); +``` + +## Key Features + +### 1. Type Inference + +Path parameters, query parameters, and body are automatically typed: + +```ts +const handler = async (request) => { + // TypeScript knows the exact types! + const { a, b } = request.validatedQuery; // { a: number; b: number } + const result = a + b; + // ... +}; +``` + +### 2. Content Negotiation + +The handler checks the `Content-Type` header to return the appropriate format: + +```ts +const handler = async (request) => { + const contentType = request.validatedHeaders.get("content-type"); + + if (contentType === "text/html") { + return request.respond({ + status: 200, + contentType: "text/html", + body: formatCalculateResponseHTML(result), + }); + } + + if (contentType === "application/xml") { + return request.respond({ + status: 200, + contentType: "application/xml", + body: formatCalculateResponseXML(result), + }); + } + + // Default to JSON + return request.respond({ + status: 200, + contentType: "application/json", + body: { result }, + }); +}; +``` + +### 3. Validation + +Invalid requests are automatically rejected: + +```ts +// Request: GET /calculate?a=150&b=50 +// Response: 400 Bad Request +// { +// "error": "Validation failed", +// "details": [ +// { +// "path": ["a"], +// "message": "Number must be less than or equal to 100" +// } +// ] +// } +``` + +### 4. OpenAPI Integration + +The example generates and serves an OpenAPI specification: + +```ts +const openApiSpec = await createOpenApiSpecification(contract, { + title: "Simple API", + version: "1.0.0", + description: "A simple calculation API", +}); + +// Serve as a route +const router = createRouter({ + contract: { + ...contract, + getSpec: { + path: "/openapi.json", + method: "GET", + responses: { + 200: { "application/json": { body: z.any() } }, + }, + }, + }, + handlers: { + ...handlers, + getSpec: async (request) => { + return request.respond({ + status: 200, + contentType: "application/json", + body: openApiSpec, + }); + }, + }, +}); +``` + +## Running the Example + +```bash +cd examples/simple +npm install +npm run dev +``` + +The server will start on `http://localhost:3000`. + +## Testing + +### GET Request + +```bash +# JSON response +curl "http://localhost:3000/calculate?a=10&b=20" \ + -H "Content-Type: application/json" + +# HTML response +curl "http://localhost:3000/calculate?a=10&b=20" \ + -H "Content-Type: text/html" + +# XML response +curl "http://localhost:3000/calculate?a=10&b=20" \ + -H "Content-Type: application/xml" +``` + +### POST Request + +```bash +curl -X POST "http://localhost:3000/calculate" \ + -H "Content-Type: application/json" \ + -d '{"a": 10, "b": 20}' +``` + +### OpenAPI Spec + +```bash +curl "http://localhost:3000/openapi.json" +``` + +## What to Learn + +This example demonstrates: +- Basic contract definition +- Handler implementation +- Type inference in action +- Content negotiation +- OpenAPI generation + +## Next Steps + +- Check out the [Complex Example](/examples/complex) for more advanced patterns +- Read the [Contracts Guide](/guide/contracts) to learn more about contracts +- Explore [Content Types](/guide/content-types) for content negotiation + diff --git a/docs-site/examples/valibot.md b/docs-site/examples/valibot.md new file mode 100644 index 0000000..538f103 --- /dev/null +++ b/docs-site/examples/valibot.md @@ -0,0 +1,205 @@ +# Valibot Example + +An example using Valibot instead of Zod for schema validation. + +## Overview + +This example demonstrates: +- Using Valibot schemas in contracts +- Valibot-specific patterns +- OpenAPI generation with Valibot +- Differences from Zod + +## Valibot vs Zod + +### Similarities + +Both libraries: +- Support Standard Schema V1 +- Provide type inference +- Support OpenAPI generation +- Have similar APIs + +### Differences + +**Valibot:** +- Smaller bundle size +- More modular API +- Uses pipes for transformations + +**Zod:** +- More mature ecosystem +- Slightly better TypeScript inference +- Method chaining API + +## Contract Definition + +```ts +import { createContract } from "itty-spec"; +import * as v from "valibot"; + +const CalculateRequest = v.pipe( + v.object({ + a: v.pipe( + v.string(), + v.toNumber(), + v.minValue(0), + v.maxValue(100), + v.title("Left Operand"), + v.description("The left operand for the calculation") + ), + b: v.pipe( + v.string(), + v.toNumber(), + v.minValue(0), + v.maxValue(100), + v.title("Right Operand"), + v.description("The right operand for the calculation") + ), + }), + v.title("Calculate Request"), + v.description("The request to calculate the sum of two numbers") +); + +export const contract = createContract({ + getCalculate: { + path: "/calculate", + method: "GET", + query: CalculateRequest, + responses: { + 200: { + "application/json": { + body: v.pipe( + v.object({ + result: v.pipe( + v.number(), + v.title("Result"), + v.description("The result of the calculation") + ), + }), + v.title("Calculate Response") + ), + }, + }, + }, + }, +}); +``` + +## Key Patterns + +### Using Pipes + +Valibot uses pipes for transformations: + +```ts +const schema = v.pipe( + v.string(), + v.toNumber(), + v.minValue(0), + v.maxValue(100) +); +``` + +### Object Schemas + +```ts +const UserSchema = v.object({ + id: v.pipe(v.string(), v.uuid()), + name: v.pipe(v.string(), v.minLength(1)), + email: v.pipe(v.string(), v.email()), +}); +``` + +### Optional Fields + +```ts +const schema = v.object({ + name: v.string(), + email: v.optional(v.pipe(v.string(), v.email())), + age: v.optional(v.pipe(v.number(), v.minValue(18))), +}); +``` + +## Handler Implementation + +Handlers work the same way as with Zod: + +```ts +const handler = async (request) => { + // Type inference works the same + const { a, b } = request.validatedQuery; // { a: number; b: number } + const result = a + b; + + return request.respond({ + status: 200, + contentType: "application/json", + body: { result }, + }); +}; +``` + +## OpenAPI Generation + +OpenAPI generation works the same: + +```ts +import { createOpenApiSpecification } from "itty-spec/openapi"; + +const openApiSpec = await createOpenApiSpecification(contract, { + title: "Valibot API", + version: "1.0.0", + description: "API using Valibot schemas", +}); +``` + +## Migration from Zod + +If migrating from Zod to Valibot: + +### Before (Zod) + +```ts +import { z } from "zod"; + +const schema = z.object({ + id: z.string().uuid(), + name: z.string().min(1), + email: z.string().email(), +}); +``` + +### After (Valibot) + +```ts +import * as v from "valibot"; + +const schema = v.object({ + id: v.pipe(v.string(), v.uuid()), + name: v.pipe(v.string(), v.minLength(1)), + email: v.pipe(v.string(), v.email()), +}); +``` + +## Running the Example + +```bash +cd examples/valibot +npm install +npm run dev +``` + +## What to Learn + +This example demonstrates: +- Valibot schema definitions +- Pipe-based transformations +- Type inference with Valibot +- OpenAPI generation +- Migration patterns + +## Related + +- [Schema Libraries Guide](/guide/schema-libraries) - Learn about schema library support +- [Simple Example](/examples/simple) - Compare with Zod example + diff --git a/docs-site/guide/advanced-patterns.md b/docs-site/guide/advanced-patterns.md new file mode 100644 index 0000000..beed4e6 --- /dev/null +++ b/docs-site/guide/advanced-patterns.md @@ -0,0 +1,446 @@ +# Advanced Patterns + +This guide covers advanced patterns and techniques for building complex APIs with `itty-spec`. + +## Multi-Domain Contracts + +Organize large APIs by splitting contracts into domains: + +```ts +// contracts/users.contract.ts +export const usersContract = createContract({ + getUser: { /* ... */ }, + createUser: { /* ... */ }, +}); + +// contracts/products.contract.ts +export const productsContract = createContract({ + getProduct: { /* ... */ }, + createProduct: { /* ... */ }, +}); + +// contracts/orders.contract.ts +export const ordersContract = createContract({ + getOrder: { /* ... */ }, + createOrder: { /* ... */ }, +}); + +// contracts/index.ts +export const contract = { + ...usersContract, + ...productsContract, + ...ordersContract, +}; +``` + +## Contract Composition + +Compose contracts from smaller pieces: + +```ts +// contracts/base.contract.ts +export const baseContract = createContract({ + healthCheck: { + path: "/health", + method: "GET", + responses: { + 200: { + "application/json": { + body: z.object({ status: z.literal("ok") }), + }, + }, + }, + }, +}); + +// contracts/api.contract.ts +export const apiContract = { + ...baseContract, + ...usersContract, + ...productsContract, +}; +``` + +## Conditional Responses + +Handle different response types based on conditions: + +```ts +const handler = async (request) => { + const user = await getUser(request.validatedParams.id); + + if (!user) { + return request.respond({ + status: 404, + contentType: "application/json", + body: { error: "User not found" }, + }); + } + + if (user.deleted) { + return request.respond({ + status: 410, + contentType: "application/json", + body: { error: "User has been deleted" }, + }); + } + + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); +}; +``` + +## Dynamic Content Types + +Select content type based on request headers: + +```ts +const handler = async (request) => { + const accept = request.headers.get("accept") || "application/json"; + const user = await getUser(request.validatedParams.id); + + if (accept.includes("text/html")) { + return request.respond({ + status: 200, + contentType: "text/html", + body: renderUserPage(user), + }); + } + + if (accept.includes("application/xml")) { + return request.respond({ + status: 200, + contentType: "application/xml", + body: renderUserXML(user), + }); + } + + // Default to JSON + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); +}; +``` + +## File Uploads + +Handle file uploads with multipart form data: + +```ts +const contract = createContract({ + uploadFile: { + path: "/files", + method: "POST", + requests: { + "multipart/form-data": { + body: z.object({ + file: z.instanceof(File), + description: z.string().optional(), + }), + }, + }, + responses: { + 201: { + "application/json": { + body: z.object({ + id: z.string(), + filename: z.string(), + size: z.number(), + }), + }, + }, + }, + }, +}); + +const handler = async (request) => { + const formData = await request.formData(); + const file = formData.get("file") as File; + const description = formData.get("description") as string; + + // Process file + const fileId = await saveFile(file); + + return request.respond({ + status: 201, + contentType: "application/json", + body: { + id: fileId, + filename: file.name, + size: file.size, + }, + }); +}; +``` + +## Streaming Responses + +Stream large responses: + +```ts +const handler = async (request) => { + const stream = new ReadableStream({ + async start(controller) { + const data = await fetchLargeDataset(); + + for (const item of data) { + controller.enqueue(JSON.stringify(item) + "\n"); + } + + controller.close(); + }, + }); + + return new Response(stream, { + status: 200, + headers: { + "content-type": "application/x-ndjson", + }, + }); +}; +``` + +## WebSocket Integration + +While `itty-spec` is designed for HTTP, you can integrate WebSockets: + +```ts +// Handle WebSocket upgrade +const router = createRouter({ + contract: { + upgradeWebSocket: { + path: "/ws", + method: "GET", + headers: z.object({ + upgrade: z.literal("websocket"), + }), + responses: { + 101: { + "application/json": { body: z.any() }, + }, + }, + }, + }, + handlers: { + upgradeWebSocket: async (request) => { + // Handle WebSocket upgrade + // This is environment-specific + return new Response(null, { status: 101 }); + }, + }, +}); +``` + +## Request Context + +Pass additional context to handlers: + +```ts +type Context = { + db: Database; + logger: Logger; + cache: Cache; +}; + +const router = createRouter({ + contract, + handlers: { + getUser: async (request, context) => { + context.logger.info("Fetching user", { id: request.validatedParams.id }); + + const cached = await context.cache.get(request.validatedParams.id); + if (cached) { + return request.respond({ + status: 200, + contentType: "application/json", + body: cached, + }); + } + + const user = await context.db.findUser(request.validatedParams.id); + await context.cache.set(request.validatedParams.id, user); + + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); + }, + }, +}); + +// Pass context when calling +router.fetch(request, { db, logger, cache }); +``` + +## Middleware Chains + +Create reusable middleware chains: + +```ts +const authMiddleware = async (request: IRequest) => { + const auth = request.headers.get("authorization"); + if (!auth) { + throw new Error("Unauthorized"); + } + (request as any).user = await getUserFromToken(auth); +}; + +const loggingMiddleware = async (request: IRequest) => { + console.log(`${request.method} ${request.url}`); +}; + +const errorHandlingMiddleware = async (request: IRequest) => { + try { + // Your logic + } catch (error) { + // Handle error + throw error; + } +}; + +// Reuse across routers +const commonMiddleware = [ + loggingMiddleware, + authMiddleware, + errorHandlingMiddleware, +]; + +const router1 = createRouter({ + contract: contract1, + handlers: handlers1, + before: commonMiddleware, +}); + +const router2 = createRouter({ + contract: contract2, + handlers: handlers2, + before: commonMiddleware, +}); +``` + +## Versioning + +Version your API using base paths: + +```ts +const v1Router = createRouter({ + contract: v1Contract, + handlers: v1Handlers, + base: "/api/v1", +}); + +const v2Router = createRouter({ + contract: v2Contract, + handlers: v2Handlers, + base: "/api/v2", +}); + +// Combine in main router +const mainRouter = Router(); +mainRouter.all("/api/v1/*", v1Router.fetch); +mainRouter.all("/api/v2/*", v2Router.fetch); +``` + +## Database Transactions + +Handle database transactions: + +```ts +const handler = async (request) => { + const transaction = await db.beginTransaction(); + + try { + const user = await transaction.createUser(request.validatedBody); + await transaction.createUserProfile(user.id, request.validatedBody.profile); + + await transaction.commit(); + + return request.respond({ + status: 201, + contentType: "application/json", + body: user, + }); + } catch (error) { + await transaction.rollback(); + throw error; + } +}; +``` + +## Caching Strategies + +Implement caching: + +```ts +const cache = new Map(); + +const handler = async (request) => { + const cacheKey = `${request.method}:${request.url}`; + const cached = cache.get(cacheKey); + + if (cached && cached.expires > Date.now()) { + return request.respond({ + status: 200, + contentType: "application/json", + body: cached.data, + }); + } + + const data = await fetchData(); + cache.set(cacheKey, { + data, + expires: Date.now() + 60000, // 1 minute + }); + + return request.respond({ + status: 200, + contentType: "application/json", + body: data, + }); +}; +``` + +## Rate Limiting + +Implement rate limiting: + +```ts +const rateLimiter = new Map(); + +const withRateLimit = (maxRequests: number = 100, windowMs: number = 60000) => { + return async (request: IRequest) => { + const key = request.headers.get("x-forwarded-for") || "unknown"; + const now = Date.now(); + + const limit = rateLimiter.get(key); + + if (limit && limit.resetAt > now) { + if (limit.count >= maxRequests) { + throw new Error("Rate limit exceeded"); + } + limit.count++; + } else { + rateLimiter.set(key, { count: 1, resetAt: now + windowMs }); + } + }; +}; + +const router = createRouter({ + contract, + handlers, + before: [withRateLimit(100, 60000)], +}); +``` + +## Related Topics + +- [Best Practices](/guide/best-practices) - General best practices +- [Middleware](/guide/middleware) - Middleware patterns +- [Router Configuration](/guide/router-configuration) - Router setup + diff --git a/docs-site/guide/best-practices.md b/docs-site/guide/best-practices.md new file mode 100644 index 0000000..e5bc0d4 --- /dev/null +++ b/docs-site/guide/best-practices.md @@ -0,0 +1,508 @@ +# Best Practices + +This guide covers best practices for building APIs with `itty-spec`, from contract design to deployment. + +## Contract Organization + +### 1. Organize by Domain + +Split contracts by domain or feature: + +```ts +// contracts/users.contract.ts +export const usersContract = createContract({ + getUser: { /* ... */ }, + createUser: { /* ... */ }, +}); + +// contracts/products.contract.ts +export const productsContract = createContract({ + getProduct: { /* ... */ }, + createProduct: { /* ... */ }, +}); + +// contracts/index.ts +export const contract = { + ...usersContract, + ...productsContract, +}; +``` + +### 2. Use Descriptive Operation IDs + +```ts +// ✅ Good +operationId: "getUserById" +operationId: "createUserAccount" +operationId: "updateUserProfile" + +// ❌ Bad +operationId: "get" +operationId: "create" +operationId: "update" +``` + +### 3. Provide Metadata + +Always include summary, description, and tags: + +```ts +const contract = createContract({ + getUser: { + operationId: "getUserById", + summary: "Get user by ID", + description: "Retrieves a user by their unique identifier. Returns 404 if user not found.", + tags: ["Users"], + path: "/users/:id", + method: "GET", + responses: { /* ... */ }, + }, +}); +``` + +## Schema Reuse + +### 1. Define Schemas Once + +```ts +// schemas/user.ts +export const UserSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + email: z.string().email(), +}); + +export const CreateUserRequest = z.object({ + name: z.string(), + email: z.string().email(), +}); + +// Use in contracts +import { UserSchema, CreateUserRequest } from "./schemas/user"; + +const contract = createContract({ + getUser: { + responses: { + 200: { "application/json": { body: UserSchema } }, + }, + }, + createUser: { + requests: { + "application/json": { body: CreateUserRequest }, + }, + responses: { + 201: { "application/json": { body: UserSchema } }, + }, + }, +}); +``` + +### 2. Use Schema Composition + +```ts +const BaseUserSchema = z.object({ + id: z.string().uuid(), + name: z.string(), +}); + +const UserWithEmailSchema = BaseUserSchema.extend({ + email: z.string().email(), +}); + +const AdminUserSchema = UserWithEmailSchema.extend({ + role: z.literal("admin"), + permissions: z.array(z.string()), +}); +``` + +### 3. Create Common Schemas + +```ts +// schemas/common.ts +export const ErrorSchema = z.object({ + error: z.string(), + message: z.string(), + details: z.array(z.unknown()).optional(), +}); + +export const PaginationQuery = z.object({ + page: z.number().min(1).default(1), + limit: z.number().min(1).max(100).default(10), +}); + +export const PaginationResponse = z.object({ + data: z.array(z.unknown()), + meta: z.object({ + page: z.number(), + limit: z.number(), + total: z.number(), + }), +}); +``` + +## Handler Organization + +### 1. Group Handlers by Domain + +```ts +// handlers/users.handlers.ts +export const userHandlers = { + getUser: async (request) => { /* ... */ }, + createUser: async (request) => { /* ... */ }, + updateUser: async (request) => { /* ... */ }, + deleteUser: async (request) => { /* ... */ }, +}; + +// handlers/products.handlers.ts +export const productHandlers = { + getProduct: async (request) => { /* ... */ }, + createProduct: async (request) => { /* ... */ }, +}; + +// index.ts +const router = createRouter({ + contract, + handlers: { + ...userHandlers, + ...productHandlers, + }, +}); +``` + +### 2. Keep Handlers Focused + +```ts +// ✅ Good - single responsibility +const getUser = async (request) => { + const { id } = request.validatedParams; + const user = await db.findUser(id); + + if (!user) { + return request.respond({ + status: 404, + contentType: "application/json", + body: { error: "User not found" }, + }); + } + + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); +}; + +// ❌ Bad - mixed concerns +const getUser = async (request) => { + // Authentication + // Authorization + // Business logic + // Logging + // Response formatting +}; +``` + +### 3. Extract Business Logic + +```ts +// services/user.service.ts +export class UserService { + async findUser(id: string): Promise { + return await db.findUser(id); + } + + async createUser(data: CreateUserData): Promise { + return await db.createUser(data); + } +} + +// handlers/users.handlers.ts +const userService = new UserService(); + +export const userHandlers = { + getUser: async (request) => { + const user = await userService.findUser(request.validatedParams.id); + // ... + }, +}; +``` + +## Error Handling Strategies + +### 1. Define Error Responses in Contracts + +```ts +responses: { + 200: { "application/json": { body: SuccessSchema } }, + 400: { "application/json": { body: ErrorSchema } }, + 401: { "application/json": { body: ErrorSchema } }, + 404: { "application/json": { body: ErrorSchema } }, + 500: { "application/json": { body: ErrorSchema } }, +} +``` + +### 2. Use Consistent Error Format + +```ts +// ✅ Good - consistent +{ + error: "Not Found", + message: "User not found", + details: [{ resource: "user", id: "123" }] +} + +// ❌ Bad - inconsistent +{ + error: "Not Found" +} +// vs +{ + message: "User not found" +} +``` + +### 3. Handle Errors Gracefully + +```ts +const handler = async (request) => { + try { + const user = await getUser(request.validatedParams.id); + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); + } catch (error) { + if (error instanceof NotFoundError) { + return request.respond({ + status: 404, + contentType: "application/json", + body: { error: "Not Found" }, + }); + } + throw error; // Let error handler deal with it + } +}; +``` + +## Testing Strategies + +### 1. Test Contracts + +```ts +import { test, expect } from "vitest"; +import { createContract } from "itty-spec"; + +test("contract is valid", () => { + const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + responses: { + 200: { "application/json": { body: z.object({ id: z.string() }) } }, + }, + }, + }); + + expect(contract).toBeDefined(); +}); +``` + +### 2. Test Handlers + +```ts +import { test, expect } from "vitest"; +import { createRouter } from "itty-spec"; + +test("getUser handler returns user", async () => { + const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { + return request.respond({ + status: 200, + contentType: "application/json", + body: { id: "123", name: "John" }, + }); + }, + }, + }); + + const response = await router.fetch( + new Request("http://localhost/users/123") + ); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.id).toBe("123"); +}); +``` + +### 3. Test Validation + +```ts +test("validation rejects invalid input", async () => { + const router = createRouter({ + contract: { + createUser: { + path: "/users", + method: "POST", + requests: { + "application/json": { + body: z.object({ + email: z.string().email(), + }), + }, + }, + responses: { + 201: { "application/json": { body: z.object({ id: z.string() }) } }, + }, + }, + }, + handlers: { /* ... */ }, + }); + + const response = await router.fetch( + new Request("http://localhost/users", { + method: "POST", + body: JSON.stringify({ email: "not-an-email" }), + headers: { "content-type": "application/json" }, + }) + ); + + expect(response.status).toBe(400); +}); +``` + +## Performance Considerations + +### 1. Generate OpenAPI Spec Once + +```ts +// ✅ Good - generate once at startup +const openApiSpec = await createOpenApiSpecification(contract, options); + +// ❌ Bad - generate on every request +const handler = async (request) => { + const spec = await createOpenApiSpecification(contract, options); + // ... +}; +``` + +### 2. Cache Validated Data + +```ts +// If validation is expensive, cache results +const cache = new Map(); + +const handler = async (request) => { + const cacheKey = `${request.method}:${request.url}`; + if (cache.has(cacheKey)) { + return cache.get(cacheKey); + } + + const result = await expensiveOperation(); + cache.set(cacheKey, result); + return result; +}; +``` + +### 3. Use Efficient Schemas + +```ts +// ✅ Good - efficient validation +const schema = z.object({ + id: z.string().uuid(), + name: z.string(), +}); + +// ❌ Bad - unnecessary transforms +const schema = z.object({ + id: z.string().transform(uuid).pipe(z.string().uuid()), + name: z.string().transform(trim).pipe(z.string()), +}); +``` + +## Bundle Size Optimization + +### 1. Tree-Shake Unused Code + +```ts +// ✅ Good - import only what you need +import { createContract, createRouter } from "itty-spec"; + +// ❌ Bad - import everything +import * as ittySpec from "itty-spec"; +``` + +### 2. Use Valibot for Smaller Bundles + +```ts +// Valibot is smaller than Zod +import * as v from "valibot"; +``` + +### 3. Avoid Heavy Dependencies + +```ts +// ✅ Good - lightweight +import { z } from "zod"; + +// ❌ Bad - heavy +import { z } from "zod"; +import heavyLibrary from "heavy-library"; +``` + +## Security Best Practices + +### 1. Validate All Inputs + +```ts +// ✅ Good - validates everything +const contract = createContract({ + createUser: { + pathParams: z.object({ id: z.string().uuid() }), + query: z.object({ include: z.array(z.string()) }), + headers: z.object({ authorization: z.string() }), + requests: { + "application/json": { body: CreateUserSchema }, + }, + responses: { /* ... */ }, + }, +}); +``` + +### 2. Sanitize Outputs + +```ts +const handler = async (request) => { + const user = await getUser(request.validatedParams.id); + + // Remove sensitive data + const { password, ...safeUser } = user; + + return request.respond({ + status: 200, + contentType: "application/json", + body: safeUser, + }); +}; +``` + +### 3. Use HTTPS + +Always use HTTPS in production: + +```ts +// Ensure your deployment uses HTTPS +// Cloudflare Workers: Automatic +// Node.js: Use reverse proxy (nginx, etc.) +``` + +## Related Topics + +- [Contracts](/guide/contracts) - Learn about contract design +- [Router Configuration](/guide/router-configuration) - Configure your router +- [Error Handling](/guide/error-handling) - Handle errors effectively +- [Advanced Patterns](/guide/advanced-patterns) - Advanced techniques + diff --git a/docs-site/guide/content-types.md b/docs-site/guide/content-types.md new file mode 100644 index 0000000..27e0fcc --- /dev/null +++ b/docs-site/guide/content-types.md @@ -0,0 +1,458 @@ +# Content Types + +`itty-spec` supports multiple content types for both requests and responses, enabling content negotiation and flexible API design. + +## Request Content Types + +You can define multiple request body schemas, each for a different content type: + +```ts +const contract = createContract({ + createUser: { + path: "/users", + method: "POST", + requests: { + "application/json": { + body: z.object({ + name: z.string(), + email: z.string().email(), + }), + }, + "application/xml": { + body: z.string(), // XML as string + }, + "application/x-www-form-urlencoded": { + body: z.object({ + name: z.string(), + email: z.string().email(), + }), + }, + }, + responses: { + 201: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); +``` + +### Content-Type Matching + +The router matches the request's `Content-Type` header to the appropriate schema: + +```ts +// Request with Content-Type: application/json +// → Validates against JSON schema + +// Request with Content-Type: application/xml +// → Validates against XML schema +``` + +### Handling Multiple Content Types in Handlers + +In your handler, you can check the content type and handle accordingly: + +```ts +const handler = async (request) => { + const contentType = request.headers.get("content-type"); + const body = request.validatedBody; + + if (contentType?.includes("json")) { + // body is typed from JSON schema + const { name, email } = body as { name: string; email: string }; + // ... + } else if (contentType?.includes("xml")) { + // body is typed as string + const xmlString = body as string; + // Parse XML... + } +}; +``` + +## Response Content Types + +Define multiple response formats for the same status code: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + responses: { + 200: { + "application/json": { + body: z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + }), + }, + "text/html": { + body: z.string(), // HTML string + }, + "application/xml": { + body: z.string(), // XML string + }, + }, + }, + }, +}); +``` + +### Content Negotiation + +Use the `Accept` header to determine the response format: + +```ts +const handler = async (request) => { + const accept = request.headers.get("accept") || "application/json"; + + if (accept.includes("text/html")) { + return request.respond({ + status: 200, + contentType: "text/html", + body: generateHTML(user), + }); + } + + if (accept.includes("application/xml")) { + return request.respond({ + status: 200, + contentType: "application/xml", + body: generateXML(user), + }); + } + + // Default to JSON + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); +}; +``` + +## JSON Content Type + +JSON is the most common content type. Define JSON schemas using Zod: + +```ts +requests: { + "application/json": { + body: z.object({ + name: z.string(), + email: z.string().email(), + }), + }, +} + +responses: { + 200: { + "application/json": { + body: z.object({ + id: z.string(), + name: z.string(), + }), + }, + }, +} +``` + +## HTML Content Type + +Serve HTML responses for web pages or HTML fragments: + +```ts +responses: { + 200: { + "text/html": { + body: z.string(), // HTML as string + }, + }, +} + +// In handler: +return request.respond({ + status: 200, + contentType: "text/html", + body: ` + + +

User Profile

+

Name: ${user.name}

+ + + `, +}); +``` + +### HTML with Headers + +You can include response headers with HTML: + +```ts +responses: { + 200: { + "text/html": { + body: z.string(), + headers: z.object({ + "content-type": z.literal("text/html; charset=utf-8"), + }), + }, + }, +} +``` + +## XML Content Type + +Serve XML responses: + +```ts +responses: { + 200: { + "application/xml": { + body: z.string(), // XML as string + }, + }, +} + +// In handler: +return request.respond({ + status: 200, + contentType: "application/xml", + body: `${user.id}`, +}); +``` + +## Form Data + +Handle form submissions: + +```ts +requests: { + "application/x-www-form-urlencoded": { + body: z.object({ + name: z.string(), + email: z.string().email(), + }), + }, + "multipart/form-data": { + body: z.object({ + name: z.string(), + email: z.string().email(), + file: z.instanceof(File).optional(), + }), + }, +} +``` + +### Parsing Form Data + +For form data, you may need to parse it manually: + +```ts +const handler = async (request) => { + const contentType = request.headers.get("content-type"); + + if (contentType?.includes("form-urlencoded")) { + const formData = await request.formData(); + const name = formData.get("name"); + const email = formData.get("email"); + // ... + } +}; +``` + +## Custom Content Types + +Define custom content types for specialized formats: + +```ts +requests: { + "application/vnd.api+json": { // JSON:API format + body: z.object({ + data: z.object({ + type: z.string(), + attributes: z.record(z.unknown()), + }), + }), + }, + "text/csv": { + body: z.string(), // CSV as string + }, +} +``` + +## Content-Type Best Practices + +### 1. Always Specify Content-Type + +```ts +// ✅ Good - explicit content type +return request.respond({ + status: 200, + contentType: "application/json", + body: data, +}); + +// ❌ Bad - missing content type +return request.respond({ + status: 200, + body: data, +}); +``` + +### 2. Use Appropriate Content Types + +```ts +// ✅ Good +"application/json" // For JSON data +"text/html" // For HTML +"application/xml" // For XML +"text/plain" // For plain text + +// ❌ Bad +"json" // Not a valid content type +"html" // Not a valid content type +``` + +### 3. Handle Content Negotiation + +```ts +const handler = async (request) => { + const accept = request.headers.get("accept") || "application/json"; + + // Prioritize requested format + if (accept.includes("text/html")) { + return htmlResponse(); + } + + // Fallback to JSON + return jsonResponse(); +}; +``` + +### 4. Validate Content-Type Headers + +```ts +headers: z.object({ + "content-type": z.union([ + z.literal("application/json"), + z.literal("application/xml"), + ]), +}) +``` + +### 5. Use Consistent Content Types + +```ts +// ✅ Good - consistent across operations +responses: { + 200: { + "application/json": { body: UserSchema }, + }, +} + +// ❌ Bad - inconsistent +responses: { + 200: { + "application/json": { body: UserSchema }, + "text/json": { body: UserSchema }, // Non-standard + }, +} +``` + +## Examples + +### JSON API + +```ts +const contract = createContract({ + getUsers: { + path: "/users", + method: "GET", + responses: { + 200: { + "application/json": { + body: z.object({ + data: z.array(UserSchema), + meta: z.object({ + total: z.number(), + page: z.number(), + }), + }), + }, + }, + }, + }, +}); +``` + +### HTML Page + +```ts +const contract = createContract({ + getDashboard: { + path: "/dashboard", + method: "GET", + responses: { + 200: { + "text/html": { + body: z.string(), + }, + }, + }, + }, +}); + +// Handler returns HTML +const handler = async (request) => { + return request.respond({ + status: 200, + contentType: "text/html", + body: renderDashboard(), + }); +}; +``` + +### XML API + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + responses: { + 200: { + "application/xml": { + body: z.string(), + }, + }, + }, + }, +}); + +// Handler returns XML +const handler = async (request) => { + const user = await getUser(request.validatedParams.id); + return request.respond({ + status: 200, + contentType: "application/xml", + body: ` + + + ${user.id} + ${user.name} + + `, + }); +}; +``` + +## Related Topics + +- [Contracts](/guide/contracts) - Learn about defining content types in contracts +- [Validation](/guide/validation) - Understand how content types affect validation +- [Examples](/examples/content-types) - See content type examples + diff --git a/docs-site/guide/contracts.md b/docs-site/guide/contracts.md new file mode 100644 index 0000000..ccf3048 --- /dev/null +++ b/docs-site/guide/contracts.md @@ -0,0 +1,503 @@ +# Contracts Deep Dive + +Contracts are the foundation of `itty-spec`. They define your API's structure, validation rules, and type information in a single, declarative format. + +## Contract Operation Structure + +A contract operation defines a single API endpoint with all its inputs and outputs: + +```ts +{ + operationId?: string; // Optional operation identifier + summary?: string; // Short description + description?: string; // Detailed description + title?: string; // Operation title + tags?: string[]; // Tags for grouping operations + path: string; // Route pattern (required) + method: HttpMethod; // HTTP method (required) + pathParams?: Schema; // Path parameter schema + query?: Schema; // Query parameter schema + headers?: Schema; // Header schema + requests?: { // Request body schemas + [contentType: string]: { + body: Schema; + }; + }; + responses: { // Response schemas (required) + [statusCode: number]: { + [contentType: string]: { + body: Schema; + headers?: Schema; + }; + }; + }; +} +``` + +## Path Parameters + +Path parameters are extracted from URL patterns like `/users/:id` or `/posts/:postId/comments/:commentId`. + +### Automatic Extraction + +When you use a path pattern with `:param`, `itty-spec` automatically extracts and types the parameters: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", // Automatically extracts { id: string } + method: "GET", + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); + +// In your handler: +const { id } = request.validatedParams; // { id: string } +``` + +**Important**: For automatic extraction to work with full type inference, use `as const`: + +```ts +// ✅ Good - full type inference +const contract = createContract({ + getUser: { + path: "/users/:id", + // ... + }, +} as const); + +// ⚠️ May work but type inference may be limited +const contract = createContract({ + getUser: { + path: "/users/:id", + // ... + }, +}); +``` + +### Explicit Path Parameter Schemas + +For validation beyond string types, provide an explicit `pathParams` schema: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ + id: z.string().uuid(), // Validate as UUID + }), + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); +``` + +### Multiple Path Parameters + +Extract multiple parameters from complex paths: + +```ts +const contract = createContract({ + getComment: { + path: "/posts/:postId/comments/:commentId", + method: "GET", + // Automatically extracts { postId: string; commentId: string } + responses: { + 200: { + "application/json": { body: CommentSchema }, + }, + }, + }, +}); +``` + +## Query Parameters + +Query parameters are parsed from the URL query string and validated against your schema. + +### Basic Query Parameters + +```ts +const contract = createContract({ + searchUsers: { + path: "/users", + method: "GET", + query: z.object({ + q: z.string().min(1), // Required + limit: z.number().default(10), // Optional with default + offset: z.number().optional(), // Optional + }), + responses: { + 200: { + "application/json": { body: z.array(UserSchema) }, + }, + }, + }, +}); +``` + +### Query Parameter Types + +Query parameters are always strings in URLs, but you can transform them: + +```ts +query: z.object({ + page: z.string().transform(Number).pipe(z.number().min(1)), + limit: z.string().transform(Number).pipe(z.number().min(1).max(100)), + tags: z.string().transform(s => s.split(',')), // "tag1,tag2" -> ["tag1", "tag2"] + active: z.enum(['true', 'false']).transform(val => val === 'true'), +}) +``` + +### Array Query Parameters + +Handle array parameters (e.g., `?tags=tag1&tags=tag2`): + +```ts +query: z.object({ + tags: z.array(z.string()).optional(), + ids: z.array(z.string().uuid()), +}) +``` + +## Headers + +Headers are validated and normalized to lowercase keys for consistent access. + +### Basic Header Validation + +```ts +const contract = createContract({ + createUser: { + path: "/users", + method: "POST", + headers: z.object({ + authorization: z.string(), + "content-type": z.literal("application/json"), + "x-api-key": z.string(), + }), + responses: { + 201: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); +``` + +### Typed Headers + +Headers are automatically typed in your handlers: + +```ts +// Headers are normalized to lowercase +const auth = request.validatedHeaders.get("authorization"); // string | null +const apiKey = request.validatedHeaders.get("x-api-key"); // string | null + +// TypeScript provides autocomplete for known headers +request.validatedHeaders.set("authorization", "Bearer token"); +``` + +### Header Normalization + +All header keys are normalized to lowercase at runtime, regardless of how they're defined in your schema: + +```ts +// Schema definition +headers: z.object({ + "Authorization": z.string(), // Capital A + "X-API-Key": z.string(), // Mixed case +}) + +// Runtime access (always lowercase) +request.validatedHeaders.get("authorization"); // ✅ Works +request.validatedHeaders.get("x-api-key"); // ✅ Works +``` + +## Request Bodies + +Request bodies are validated based on the `Content-Type` header and can support multiple content types. + +### Single Content Type + +```ts +const contract = createContract({ + createUser: { + path: "/users", + method: "POST", + requests: { + "application/json": { + body: z.object({ + name: z.string(), + email: z.string().email(), + }), + }, + }, + responses: { + 201: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); +``` + +### Multiple Content Types + +Support different request formats: + +```ts +const contract = createContract({ + createUser: { + path: "/users", + method: "POST", + requests: { + "application/json": { + body: z.object({ + name: z.string(), + email: z.string().email(), + }), + }, + "application/xml": { + body: z.string(), // XML as string + }, + }, + responses: { + 201: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); + +// In handler, validate body based on Content-Type +const contentType = request.headers.get("content-type"); +if (contentType?.includes("json")) { + const { name, email } = request.validatedBody; // Typed from JSON schema +} +``` + +## Responses + +Responses define all possible status codes and content types your endpoint can return. + +### Single Response + +```ts +responses: { + 200: { + "application/json": { + body: z.object({ + users: z.array(UserSchema), + total: z.number(), + }), + }, + }, +} +``` + +### Multiple Status Codes + +Define different responses for different scenarios: + +```ts +responses: { + 200: { + "application/json": { + body: UserSchema, + }, + }, + 400: { + "application/json": { + body: z.object({ error: z.string() }), + }, + }, + 404: { + "application/json": { + body: z.object({ error: z.string() }), + }, + }, +} +``` + +### Multiple Content Types + +Support content negotiation: + +```ts +responses: { + 200: { + "application/json": { + body: UserSchema, + }, + "text/html": { + body: z.string(), // HTML string + }, + "application/xml": { + body: z.string(), // XML string + }, + }, +} +``` + +### Response Headers + +Define response headers in your contract: + +```ts +responses: { + 201: { + "application/json": { + body: UserSchema, + headers: z.object({ + location: z.string().url(), + "x-created-at": z.string(), + }), + }, + }, +} + +// In handler: +return request.respond({ + status: 201, + contentType: "application/json", + body: user, + headers: { + location: `/users/${user.id}`, + "x-created-at": new Date().toISOString(), + }, +}); +``` + +### Default Responses + +When 200 is not present, you must provide a `default` response: + +```ts +responses: { + 400: { + "application/json": { body: ErrorSchema }, + }, + default: { // Required when 200 is missing + "application/json": { body: ErrorSchema }, + }, +} +``` + +## Operation Metadata + +Add metadata to improve documentation and OpenAPI generation: + +```ts +const contract = createContract({ + getUser: { + operationId: "getUserById", // Unique identifier + summary: "Get user by ID", // Short description + description: "Retrieves a user...", // Detailed description + title: "Get User", // Display title + tags: ["Users", "Public"], // Grouping tags + path: "/users/:id", + method: "GET", + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); +``` + +## Best Practices + +### 1. Use Descriptive Operation IDs + +```ts +// ✅ Good +operationId: "getUserById" +operationId: "createUserAccount" + +// ❌ Bad +operationId: "get" +operationId: "create" +``` + +### 2. Reuse Schemas + +```ts +// Define schemas once +const UserSchema = z.object({ /* ... */ }); +const ErrorSchema = z.object({ error: z.string() }); + +// Reuse in contracts +const contract = createContract({ + getUser: { + // ... + responses: { + 200: { "application/json": { body: UserSchema } }, + 404: { "application/json": { body: ErrorSchema } }, + }, + }, + createUser: { + // ... + responses: { + 201: { "application/json": { body: UserSchema } }, + 400: { "application/json": { body: ErrorSchema } }, + }, + }, +}); +``` + +### 3. Use `as const` for Better Type Inference + +```ts +// ✅ Good - full type inference +const contract = createContract({ + getUser: { + path: "/users/:id", + // ... + }, +} as const); +``` + +### 4. Validate Path Parameters Explicitly + +For non-string path parameters, use explicit schemas: + +```ts +// ✅ Good - validates UUID format +pathParams: z.object({ + id: z.string().uuid(), +}) +``` + +### 5. Provide Defaults for Optional Query Parameters + +```ts +// ✅ Good +query: z.object({ + page: z.number().min(1).default(1), + limit: z.number().min(1).max(100).default(10), +}) +``` + +### 6. Use Tags for Organization + +```ts +tags: ["Users", "Authentication"] // Groups operations in OpenAPI docs +``` + +## Related Topics + +- [Type Safety](/guide/type-safety) - Learn how types flow from contracts +- [Validation](/guide/validation) - Understand validation behavior +- [Router Configuration](/guide/router-configuration) - Configure your router +- [OpenAPI Integration](/guide/openapi) - Generate API documentation + diff --git a/docs-site/guide/core-concepts.md b/docs-site/guide/core-concepts.md index 9476370..8922366 100644 --- a/docs-site/guide/core-concepts.md +++ b/docs-site/guide/core-concepts.md @@ -1,83 +1,265 @@ # Core Concepts +This guide explains the fundamental concepts behind `itty-spec` and how they work together to provide type-safe, contract-first API development. + ## Contract -A contract is a plain object describing each operation: +A contract is a plain object describing each operation in your API. It serves as the single source of truth for both runtime behavior and compile-time types. -* `method` and `path` -* optional schemas for `path params`, `query`, `headers`, and request bodies -* allowed `responses` keyed by status code and content type +### Contract Structure -The contract drives both runtime behavior (validation + routing) and compile-time types. +Each operation in a contract defines: -## Router +* `method` and `path` - HTTP method and route pattern +* `pathParams` (optional) - Schema for path parameters (e.g., `/users/:id`) +* `query` (optional) - Schema for query string parameters +* `headers` (optional) - Schema for request headers +* `requests` (optional) - Request body schemas keyed by content type +* `responses` - Response schemas keyed by status code and content type +* Metadata (optional) - `summary`, `description`, `tags`, `operationId` -`createRouter({ contract, handlers })` binds your handlers to the contract and produces a Fetch handler (`router.fetch`). +### Example Contract -Before a handler is called, `itty-spec` validates the incoming request according to the schemas you provided. Your handler receives a request object with typed, validated data (for example `request.validatedQuery` and `request.validatedBody`). +```ts +import { createContract } from "itty-spec"; +import { z } from "zod"; -## Responses +export const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ id: z.string().uuid() }), + headers: z.object({ + authorization: z.string(), + }), + responses: { + 200: { + "application/json": { + body: z.object({ + id: z.string().uuid(), + name: z.string(), + email: z.string().email(), + }), + }, + }, + 404: { + "application/json": { + body: z.object({ error: z.string() }), + }, + }, + }, + }, +}); +``` -Handlers return responses via `request.respond({ status, contentType, body })`. +### Contract Benefits -The shape of that response is type-checked against the contract for the current operation, so returning the wrong status code, content type, or body shape becomes a TypeScript error. +The contract drives both: +- **Runtime behavior**: Automatic validation, routing, and response formatting +- **Compile-time types**: Full TypeScript inference from contract to handler -## Schema Support +## Router -`itty-spec` uses the [Standard Schema V1](https://github.com/standard-schema/spec) interface, which provides a common abstraction layer for schema validation. This means you can use any Standard Schema V1 compatible library: +`createRouter({ contract, handlers })` binds your handlers to the contract and produces a Fetch handler (`router.fetch`). -* **Zod (v4)**: Fully supported with excellent TypeScript inference and OpenAPI generation. Recommended for the best developer experience. -* **Valibot**: Fully supported with OpenAPI generation via `@standard-community/standard-openapi`. -* **Other Standard Schema compatible libraries**: Can be used for validation; OpenAPI support depends on the library's Standard Schema V1 implementation. +### Router Lifecycle -The Standard Schema V1 interface ensures that your contracts remain portable across different schema libraries while maintaining type safety and runtime validation. +Here's what happens when a request comes in: + +```mermaid +flowchart TD + A[Incoming Request] --> B[Match Operation] + B --> C[Validate Path Params] + C --> D[Validate Query Params] + D --> E[Validate Headers] + E --> F[Validate Body] + F --> G[Run Before Middleware] + G --> H[Execute Handler] + H --> I[Format Response] + I --> J[Run Finally Middleware] + J --> K[Return Response] + + C -->|Validation Fails| L[Return 400 Error] + D -->|Validation Fails| L + E -->|Validation Fails| L + F -->|Validation Fails| L + H -->|Handler Throws| M[Error Handler] + M --> K +``` -## OpenAPI 3.1 Generation and Serving (Optional) +### Request Flow -Generate an OpenAPI 3.1 specification directly from your contract and serve it as a documentation endpoint: +1. **Operation Matching**: The router finds the matching operation based on HTTP method and path pattern +2. **Validation**: All request parts (params, query, headers, body) are validated against schemas +3. **Handler Execution**: Your handler receives typed, validated data +4. **Response Formatting**: The response is validated against the contract and formatted + +### Example Router ```ts -import { createOpenApiSpecification } from "itty-spec/openapi"; import { createRouter } from "itty-spec"; import { contract } from "./contract"; -import { z } from "zod"; -// Generate the OpenAPI spec -const openApiSpec = await createOpenApiSpecification(contract, { - title: "My API", - version: "1.0.0", - description: "Example API built with itty-spec", - servers: [{ url: "https://api.example.com", description: "Production" }], -}); - -// Serve it as a route in your router const router = createRouter({ - contract: { - ...contract, - getSpec: { - path: "/openapi.json", - method: "GET", - responses: { - 200: { - "application/json": { body: z.any() }, - }, - }, - }, - }, + contract, handlers: { - ...yourHandlers, - getSpec: async (request) => { + getUser: async (request) => { + // All data is typed and validated! + const { id } = request.validatedParams; // { id: string } + const authHeader = request.validatedHeaders.get("authorization"); // string | null + + // Your business logic here + const user = await getUserById(id); + return request.respond({ status: 200, contentType: "application/json", - body: openApiSpec, + body: user, // TypeScript ensures this matches the contract }); }, }, }); ``` -OpenAPI generation uses `@standard-community/standard-openapi` to convert Standard Schema V1 schemas to OpenAPI 3.1 format. This supports Zod v4 and Valibot schemas out of the box. You can then use tools like [Swagger UI](https://swagger.io/tools/swagger-ui/), [Redoc](https://github.com/Redocly/redoc), or [Elements](https://github.com/stoplightio/elements) to render interactive documentation from the served specification. +## Validation Flow + +Before your handler runs, `itty-spec` validates the incoming request according to the schemas you provided. This happens automatically in the middleware chain. + +### Validation Order + +1. **Path Parameters**: Extracted from URL and validated +2. **Query Parameters**: Parsed from URL and validated +3. **Headers**: Normalized (lowercase) and validated +4. **Body**: Parsed based on Content-Type and validated + +### Validation Errors + +If validation fails, the request is rejected with a 400 status code and detailed error information: + +```json +{ + "error": "Validation failed", + "details": [ + { + "path": ["email"], + "message": "Invalid email" + } + ] +} +``` + +## Type Inference + +`itty-spec` provides end-to-end type safety through TypeScript inference. Types flow from your contract schemas directly to your handlers. + +### Type Flow + +```mermaid +flowchart LR + A[Contract Schema] --> B[Type Inference] + B --> C[Handler Types] + C --> D[Request.validatedParams] + C --> E[Request.validatedQuery] + C --> F[Request.validatedBody] + C --> G[Request.validatedHeaders] + C --> H[Response Types] + + style A fill:#e1f5ff + style B fill:#fff4e1 + style C fill:#e8f5e9 +``` + +### Example Type Inference + +```ts +// Contract defines the types +const contract = createContract({ + createUser: { + path: "/users", + method: "POST", + requests: { + "application/json": { + body: z.object({ + name: z.string(), + email: z.string().email(), + }), + }, + }, + responses: { + 201: { + "application/json": { + body: z.object({ id: z.string(), name: z.string() }), + }, + }, + }, + }, +}); + +// Handler receives inferred types +const handler = async (request: ContractRequest) => { + // TypeScript knows the exact shape! + const { name, email } = request.validatedBody; // { name: string; email: string } + + // TypeScript ensures response matches contract + return request.respond({ + status: 201, + contentType: "application/json", + body: { id: "123", name }, // ✅ Type-safe! + // body: { name } // ❌ Error: missing 'id' + }); +}; +``` + +## Responses + +Handlers return responses via `request.respond({ status, contentType, body })`. + +### Response Validation + +The shape of your response is type-checked against the contract for the current operation. Returning the wrong status code, content type, or body shape becomes a TypeScript error at compile time. + +### Multiple Response Types + +You can define multiple response variants per operation: + +```ts +responses: { + 200: { + "application/json": { body: SuccessSchema }, + "text/html": { body: z.string() }, + }, + 400: { + "application/json": { body: ErrorSchema }, + }, +} +``` + +### Response Headers + +You can also define response headers in your contract: + +```ts +responses: { + 201: { + "application/json": { + body: UserSchema, + headers: z.object({ + location: z.string().url(), + }), + }, + }, +} +``` + +## Schema Support + +`itty-spec` uses the [Standard Schema V1](https://github.com/standard-schema/spec) interface, which provides a common abstraction layer for schema validation. This means you can use any Standard Schema V1 compatible library: + +* **Zod (v4)**: Fully supported with excellent TypeScript inference and OpenAPI generation. Recommended for the best developer experience. +* **Valibot**: Fully supported with OpenAPI generation via `@standard-community/standard-openapi`. +* **Other Standard Schema compatible libraries**: Can be used for validation; OpenAPI support depends on the library's Standard Schema V1 implementation. + +The Standard Schema V1 interface ensures that your contracts remain portable across different schema libraries while maintaining type safety and runtime validation. -See the `examples/simple` and `examples/complex` directories for complete examples of serving OpenAPI documentation. +Learn more about [Schema Libraries](/guide/schema-libraries) and [OpenAPI Integration](/guide/openapi). diff --git a/docs-site/guide/error-handling.md b/docs-site/guide/error-handling.md new file mode 100644 index 0000000..5f40ead --- /dev/null +++ b/docs-site/guide/error-handling.md @@ -0,0 +1,423 @@ +# Error Handling + +`itty-spec` provides built-in error handling that automatically converts errors into appropriate HTTP responses. + +## Default Error Handling + +By default, `itty-spec` includes an error handler that catches all errors and converts them to JSON responses: + +```ts +// Built-in error handler +catch: withContractErrorHandler() +``` + +### Validation Errors + +When validation fails, errors are automatically caught and returned as 400 Bad Request: + +```json +{ + "error": "Validation failed", + "details": [ + { + "path": ["email"], + "message": "Invalid email" + } + ] +} +``` + +### Other Errors + +Other errors are converted to 500 Internal Server Error: + +```json +{ + "error": "Internal server error", + "details": [ + { + "message": "Error message here" + } + ] +} +``` + +## Error Types + +### Validation Errors + +Validation errors occur when request data doesn't match the contract schema: + +```ts +// Request with invalid email +POST /users +{ "email": "not-an-email" } + +// Response: 400 Bad Request +{ + "error": "Validation failed", + "details": [ + { + "path": ["email"], + "message": "Invalid email" + } + ] +} +``` + +### Handler Errors + +Errors thrown in handlers are caught and converted to 500 responses: + +```ts +const handler = async (request) => { + throw new Error("Something went wrong"); + // Automatically converted to 500 response +}; +``` + +### Middleware Errors + +Errors in middleware are also caught: + +```ts +const router = createRouter({ + contract, + handlers, + before: [ + async (request) => { + throw new Error("Middleware error"); + // Caught by error handler + }, + ], +}); +``` + +## Custom Error Responses + +### Error Response Contracts + +Define error responses in your contract: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + 404: { + "application/json": { + body: z.object({ + error: z.string(), + message: z.string(), + }), + }, + }, + 500: { + "application/json": { + body: z.object({ + error: z.string(), + details: z.array(z.unknown()), + }), + }, + }, + }, + }, +}); +``` + +### Throwing Errors with Status Codes + +Use itty-router's `error` helper to throw errors with specific status codes: + +```ts +import { error } from "itty-router"; + +const handler = async (request) => { + const user = await getUser(request.validatedParams.id); + + if (!user) { + throw error(404, "User not found"); + // Returns 404 response + } + + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); +}; +``` + +### Custom Error Classes + +Create custom error classes for different error types: + +```ts +class NotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = "NotFoundError"; + } +} + +class ValidationError extends Error { + constructor(message: string, public issues: unknown[]) { + super(message); + this.name = "ValidationError"; + } +} + +// In handler +const handler = async (request) => { + const user = await getUser(request.validatedParams.id); + + if (!user) { + throw new NotFoundError("User not found"); + } + + // Error handler can check error type +}; +``` + +## Custom Error Handler + +You can customize error handling by providing your own error handler: + +```ts +const router = Router({ + catch: (error, request) => { + // Custom error handling + if (error instanceof NotFoundError) { + return new Response( + JSON.stringify({ error: error.message }), + { status: 404, headers: { "content-type": "application/json" } } + ); + } + + if (error instanceof ValidationError) { + return new Response( + JSON.stringify({ + error: "Validation failed", + details: error.issues, + }), + { status: 400, headers: { "content-type": "application/json" } } + ); + } + + // Default error response + return new Response( + JSON.stringify({ error: "Internal server error" }), + { status: 500, headers: { "content-type": "application/json" } } + ); + }, +}); +``` + +## Error Patterns + +### Not Found Pattern + +```ts +const handler = async (request) => { + const resource = await findResource(request.validatedParams.id); + + if (!resource) { + return request.respond({ + status: 404, + contentType: "application/json", + body: { + error: "Not Found", + message: `Resource ${request.validatedParams.id} not found`, + }, + }); + } + + return request.respond({ + status: 200, + contentType: "application/json", + body: resource, + }); +}; +``` + +### Unauthorized Pattern + +```ts +const handler = async (request) => { + const user = await getCurrentUser(request); + + if (!user) { + return request.respond({ + status: 401, + contentType: "application/json", + body: { + error: "Unauthorized", + message: "Authentication required", + }, + }); + } + + // Continue with handler logic +}; +``` + +### Forbidden Pattern + +```ts +const handler = async (request) => { + const user = await getCurrentUser(request); + const resource = await getResource(request.validatedParams.id); + + if (user.id !== resource.ownerId) { + return request.respond({ + status: 403, + contentType: "application/json", + body: { + error: "Forbidden", + message: "You don't have permission to access this resource", + }, + }); + } + + // Continue with handler logic +}; +``` + +### Validation Error Pattern + +```ts +const handler = async (request) => { + try { + // Business logic validation + if (request.validatedBody.email.includes("spam")) { + return request.respond({ + status: 400, + contentType: "application/json", + body: { + error: "Validation failed", + message: "Email domain not allowed", + }, + }); + } + + // Continue + } catch (error) { + // Handle unexpected errors + throw error; // Let error handler deal with it + } +}; +``` + +## Error Middleware + +Create middleware to handle errors consistently: + +```ts +async function withErrorHandling(request: IRequest) { + try { + // Your logic + } catch (error) { + // Transform error before it reaches error handler + if (error instanceof DatabaseError) { + throw new Error("Database error occurred"); + } + throw error; + } +} +``` + +## Best Practices + +### 1. Define Error Responses in Contracts + +```ts +// ✅ Good - error responses defined +responses: { + 200: { "application/json": { body: SuccessSchema } }, + 400: { "application/json": { body: ErrorSchema } }, + 404: { "application/json": { body: ErrorSchema } }, + 500: { "application/json": { body: ErrorSchema } }, +} +``` + +### 2. Use Consistent Error Format + +```ts +// ✅ Good - consistent format +{ + error: "Error type", + message: "Human-readable message", + details?: [...] +} + +// ❌ Bad - inconsistent format +{ + error: "Error type" +} +// vs +{ + message: "Error message" +} +``` + +### 3. Provide Helpful Error Messages + +```ts +// ✅ Good +{ + error: "Validation failed", + message: "Email must be a valid email address", + details: [{ path: ["email"], message: "Invalid email" }] +} + +// ❌ Bad +{ + error: "Validation failed" +} +``` + +### 4. Log Errors for Debugging + +```ts +const router = Router({ + catch: (error, request) => { + // Log error for debugging + console.error("Error:", error); + console.error("Request:", request.method, request.url); + + // Return user-friendly response + return new Response( + JSON.stringify({ error: "Internal server error" }), + { status: 500 } + ); + }, +}); +``` + +### 5. Don't Expose Internal Details + +```ts +// ✅ Good - user-friendly +{ + error: "Internal server error", + message: "An error occurred processing your request" +} + +// ❌ Bad - exposes internal details +{ + error: "DatabaseConnectionError", + message: "Failed to connect to database at 192.168.1.1:5432", + stack: "..." +} +``` + +## Related Topics + +- [Validation](/guide/validation) - Understand validation errors +- [Middleware](/guide/middleware) - Handle errors in middleware +- [Router Configuration](/guide/router-configuration) - Configure error handling + diff --git a/docs-site/guide/faq.md b/docs-site/guide/faq.md new file mode 100644 index 0000000..56b2226 --- /dev/null +++ b/docs-site/guide/faq.md @@ -0,0 +1,373 @@ +# FAQ + +Frequently asked questions about `itty-spec`. + +## General Questions + +### What is itty-spec? + +`itty-spec` is a contract-first, type-safe API framework built on top of `itty-router`. It provides automatic validation, type inference, and OpenAPI generation. + +### Why use itty-spec? + +- **Type Safety**: End-to-end TypeScript inference from contract to handler +- **Validation**: Automatic request validation before handlers run +- **Documentation**: Generate OpenAPI specs from your contracts +- **Lightweight**: Minimal bundle size, perfect for edge/serverless + +### How does itty-spec differ from itty-router? + +`itty-router` is a minimal router. `itty-spec` adds: +- Contract-based routing +- Automatic validation +- Type inference +- OpenAPI generation + +### Is itty-spec production-ready? + +Yes! `itty-spec` is used in production and actively maintained. + +## Contract Questions + +### Do I need to define contracts for all endpoints? + +Yes, contracts are required. They serve as the single source of truth for your API. + +### Can I use contracts without handlers? + +No, every operation in your contract needs a corresponding handler. + +### Can I have multiple contracts? + +Yes, you can split contracts by domain and combine them: + +```ts +const contract = { + ...usersContract, + ...productsContract, +}; +``` + +### How do I handle optional fields? + +Use `.optional()` or `.default()` in your schemas: + +```ts +query: z.object({ + page: z.number().optional(), + limit: z.number().default(10), +}) +``` + +## Type Safety Questions + +### Why are my path parameters typed as `EmptyObject`? + +Use `as const` when creating contracts: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + // ... + }, +} as const); +``` + +### How do I access typed request data? + +Use the validated properties: + +```ts +const { id } = request.validatedParams; +const { page } = request.validatedQuery; +const body = request.validatedBody; +const auth = request.validatedHeaders.get("authorization"); +``` + +### Can I extend the request type? + +Yes, use TypeScript generics: + +```ts +interface AuthenticatedRequest extends IRequest { + user: User; +} + +const router = createRouter({ + // ... +}); +``` + +## Validation Questions + +### What happens if validation fails? + +Validation errors return a 400 Bad Request with details: + +```json +{ + "error": "Validation failed", + "details": [...] +} +``` + +### Can I customize validation errors? + +Yes, use custom error handling: + +```ts +const router = Router({ + catch: (error, request) => { + // Custom error handling + }, +}); +``` + +### How do I validate complex data? + +Use Zod refinements or Valibot pipes: + +```ts +const schema = z.object({ + password: z.string().min(8), + confirmPassword: z.string(), +}).refine( + (data) => data.password === data.confirmPassword, + { message: "Passwords don't match" } +); +``` + +## Response Questions + +### How do I return different status codes? + +Define them in your contract and use `request.respond()`: + +```ts +responses: { + 200: { "application/json": { body: SuccessSchema } }, + 404: { "application/json": { body: ErrorSchema } }, +} + +return request.respond({ + status: 404, + contentType: "application/json", + body: { error: "Not found" }, +}); +``` + +### Can I return HTML responses? + +Yes, define HTML content type in your contract: + +```ts +responses: { + 200: { + "text/html": { body: z.string() }, + }, +} + +return request.respond({ + status: 200, + contentType: "text/html", + body: "...", +}); +``` + +### How do I set response headers? + +Define headers in your contract and include them in the response: + +```ts +responses: { + 201: { + "application/json": { + body: UserSchema, + headers: z.object({ + location: z.string().url(), + }), + }, + }, +} + +return request.respond({ + status: 201, + contentType: "application/json", + body: user, + headers: { + location: `/users/${user.id}`, + }, +}); +``` + +## Middleware Questions + +### How do I add authentication? + +Use before middleware: + +```ts +const router = createRouter({ + contract, + handlers, + before: [ + async (request) => { + const auth = request.headers.get("authorization"); + if (!auth) { + throw new Error("Unauthorized"); + } + // Attach user to request + }, + ], +}); +``` + +### Can I use Express middleware? + +No, `itty-spec` uses Fetch API middleware. You'll need to adapt Express middleware or write new middleware. + +### How do I handle CORS? + +Use finally middleware: + +```ts +const router = createRouter({ + contract, + handlers, + finally: [ + async (response, request) => { + response.headers.set("access-control-allow-origin", "*"); + return response; + }, + ], +}); +``` + +## OpenAPI Questions + +### How do I generate OpenAPI specs? + +Use `createOpenApiSpecification`: + +```ts +import { createOpenApiSpecification } from "itty-spec/openapi"; + +const spec = await createOpenApiSpecification(contract, { + title: "My API", + version: "1.0.0", +}); +``` + +### Can I customize the OpenAPI spec? + +The spec is generated from your contract. Customize it by: +- Adding metadata to operations +- Using schema descriptions +- Organizing with tags + +### How do I serve the OpenAPI spec? + +Add it as a route: + +```ts +const router = createRouter({ + contract: { + ...contract, + getOpenApiSpec: { + path: "/openapi.json", + method: "GET", + responses: { + 200: { "application/json": { body: z.any() } }, + }, + }, + }, + handlers: { + ...handlers, + getOpenApiSpec: async (request) => { + return request.respond({ + status: 200, + contentType: "application/json", + body: openApiSpec, + }); + }, + }, +}); +``` + +## Schema Library Questions + +### Which schema library should I use? + +- **Zod**: Best TypeScript inference, mature ecosystem +- **Valibot**: Smaller bundle size, similar features + +### Can I use both Zod and Valibot? + +Technically yes, but it's not recommended. Stick to one library for consistency. + +### How do I migrate between libraries? + +Since both implement Standard Schema V1, you can migrate by updating schema definitions. See the [Schema Libraries](/guide/schema-libraries) guide. + +## Performance Questions + +### Is itty-spec fast? + +Yes! `itty-spec` is designed for performance: +- Minimal overhead +- Efficient validation +- Small bundle size + +### How do I optimize bundle size? + +1. Use Valibot instead of Zod +2. Tree-shake unused code +3. Avoid heavy dependencies + +### Can I cache validation results? + +Validation happens per request. If you need caching, implement it in your handlers. + +## Deployment Questions + +### Can I use itty-spec with Cloudflare Workers? + +Yes! `itty-spec` works perfectly with Cloudflare Workers: + +```ts +export default { + fetch: router.fetch, +}; +``` + +### Can I use itty-spec with AWS Lambda? + +Yes, with a Fetch adapter: + +```ts +import { createServerAdapter } from "@whatwg-node/server"; + +const adapter = createServerAdapter(router.fetch); +export const handler = adapter; +``` + +### Can I use itty-spec with Node.js? + +Yes, use `@whatwg-node/server`: + +```ts +import { createServerAdapter } from "@whatwg-node/server"; +import { createServer } from "http"; + +const adapter = createServerAdapter(router.fetch); +const server = createServer(adapter); +server.listen(3000); +``` + +## Related Topics + +- [Getting Started](/guide/getting-started) - Learn the basics +- [Troubleshooting](/guide/troubleshooting) - Common issues +- [Examples](/examples/) - Working examples + diff --git a/docs-site/guide/getting-started.md b/docs-site/guide/getting-started.md index 5c84e0f..d3a9ec2 100644 --- a/docs-site/guide/getting-started.md +++ b/docs-site/guide/getting-started.md @@ -6,6 +6,36 @@ npm install itty-spec # or pnpm add itty-spec +# or +yarn add itty-spec +``` + +### Peer Dependencies + +`itty-spec` requires a Standard Schema V1 compatible library for validation. Install one of the following: + +```bash +# For Zod (recommended) +npm install zod@v4 + +# For Valibot +npm install valibot +``` + +## Environment Setup + +`itty-spec` works in any environment that supports the Fetch API. No special configuration is required, but you may need TypeScript configured: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "strict": true + } +} ``` ## Quick Start @@ -124,3 +154,282 @@ export default { The library's minimal dependencies and small bundle size ensure fast startup times and low memory footprint, critical for edge and serverless deployments. +## Your First API + +Let's build a complete, working API step by step. This tutorial will show you how to create a simple todo API with full type safety. + +### Step 1: Define Your Schemas + +First, create schemas for your data structures: + +```ts +import { z } from "zod"; + +const TodoSchema = z.object({ + id: z.string().uuid(), + title: z.string().min(1), + completed: z.boolean(), + createdAt: z.string().datetime(), +}); + +const CreateTodoRequest = z.object({ + title: z.string().min(1), + completed: z.boolean().default(false), +}); +``` + +### Step 2: Create Your Contract + +Define your API contract with all operations: + +```ts +import { createContract } from "itty-spec"; + +export const contract = createContract({ + getTodos: { + path: "/todos", + method: "GET", + query: z.object({ + completed: z.boolean().optional(), + limit: z.number().min(1).max(100).default(10), + }), + responses: { + 200: { + "application/json": { + body: z.object({ + todos: z.array(TodoSchema), + total: z.number(), + }), + }, + }, + }, + }, + getTodo: { + path: "/todos/:id", + method: "GET", + responses: { + 200: { + "application/json": { body: TodoSchema }, + }, + 404: { + "application/json": { body: z.object({ error: z.string() }) }, + }, + }, + }, + createTodo: { + path: "/todos", + method: "POST", + requests: { + "application/json": { body: CreateTodoRequest }, + }, + responses: { + 201: { + "application/json": { body: TodoSchema }, + }, + 400: { + "application/json": { body: z.object({ error: z.string() }) }, + }, + }, + }, +}); +``` + +### Step 3: Implement Handlers + +Create handlers that receive typed, validated data: + +```ts +import { createRouter } from "itty-spec"; +import { contract } from "./contract"; + +// Simple in-memory store +const todos: Todo[] = []; + +const router = createRouter({ + contract, + handlers: { + getTodos: async (request) => { + const { completed, limit } = request.validatedQuery; + + let filtered = todos; + if (completed !== undefined) { + filtered = todos.filter(t => t.completed === completed); + } + + return request.respond({ + status: 200, + contentType: "application/json", + body: { + todos: filtered.slice(0, limit), + total: filtered.length, + }, + }); + }, + + getTodo: async (request) => { + const { id } = request.validatedParams; + const todo = todos.find(t => t.id === id); + + if (!todo) { + return request.respond({ + status: 404, + contentType: "application/json", + body: { error: "Todo not found" }, + }); + } + + return request.respond({ + status: 200, + contentType: "application/json", + body: todo, + }); + }, + + createTodo: async (request) => { + const { title, completed } = request.validatedBody; + + const todo = { + id: crypto.randomUUID(), + title, + completed: completed ?? false, + createdAt: new Date().toISOString(), + }; + + todos.push(todo); + + return request.respond({ + status: 201, + contentType: "application/json", + body: todo, + }); + }, + }, +}); + +export default { fetch: router.fetch }; +``` + +### Step 4: Deploy + +Now you can deploy this to any Fetch-compatible environment: + +**Cloudflare Workers:** +```ts +// Already done! Just export the fetch handler +export default { fetch: router.fetch }; +``` + +**Node.js:** +```ts +import { createServer } from "http"; +import { createServerAdapter } from "@whatwg-node/server"; + +const adapter = createServerAdapter(router.fetch); +const server = createServer(adapter); +server.listen(3000); +``` + +**Bun:** +```ts +Bun.serve({ fetch: router.fetch }); +``` + +## Common Pitfalls + +### 1. Forgetting `as const` for Path Parameters + +For automatic path parameter extraction, use `as const`: + +```ts +// ✅ Good - path params are extracted +const contract = createContract({ + getUser: { + path: "/users/:id", // TypeScript infers { id: string } + method: "GET", + // ... + }, +} as const); + +// ❌ Bad - path params may not be extracted +const contract = createContract({ + getUser: { + path: "/users/:id", // May fall back to EmptyObject + method: "GET", + // ... + }, +}); +``` + +### 2. Not Providing Required Handlers + +Every operation in your contract should have a corresponding handler: + +```ts +// ✅ Good +const router = createRouter({ + contract, + handlers: { + getUsers: async (request) => { /* ... */ }, + createUser: async (request) => { /* ... */ }, + }, +}); + +// ❌ Bad - missing handler will cause runtime errors +const router = createRouter({ + contract, + handlers: { + getUsers: async (request) => { /* ... */ }, + // createUser is missing! + }, +}); +``` + +### 3. Mismatched Response Types + +Ensure your response matches the contract exactly: + +```ts +// ✅ Good +return request.respond({ + status: 200, + contentType: "application/json", + body: { users: [], total: 0 }, // Matches contract +}); + +// ❌ Bad - TypeScript error! +return request.respond({ + status: 200, + contentType: "application/json", + body: { users: [] }, // Missing 'total' field +}); +``` + +### 4. Incorrect Content-Type Handling + +When using multiple content types, ensure you handle them correctly: + +```ts +// ✅ Good - check content type from headers +const contentType = request.validatedHeaders.get("content-type"); +if (contentType === "text/html") { + return request.respond({ + status: 200, + contentType: "text/html", + body: "...", + }); +} + +// ❌ Bad - assuming content type +return request.respond({ + status: 200, + contentType: "text/html", // May not match request + body: "...", +}); +``` + +## Next Steps + +- Learn about [Core Concepts](/guide/core-concepts) to understand how itty-spec works +- Explore [Contracts](/guide/contracts) to master contract definitions +- Check out [Examples](/examples/) for real-world patterns + diff --git a/docs-site/guide/middleware.md b/docs-site/guide/middleware.md new file mode 100644 index 0000000..a04f260 --- /dev/null +++ b/docs-site/guide/middleware.md @@ -0,0 +1,462 @@ +# Middleware + +Middleware in `itty-spec` allows you to intercept and transform requests and responses at different stages of the request lifecycle. + +## Middleware Overview + +Middleware functions run at specific points in the request/response pipeline: + +```mermaid +flowchart TD + A[Request] --> B[Before Middleware] + B --> C[Validation] + C --> D[Handler] + D --> E[Response] + E --> F[Finally Middleware] + F --> G[Response to Client] + + style B fill:#e1f5ff + style F fill:#fff4e1 +``` + +## Built-in Middleware + +`itty-spec` includes several built-in middleware that run automatically: + +### Before Middleware (Automatic) + +These run in order before your handler: + +1. **`withParams`** - Extracts path parameters from URL +2. **`withMatchingContractOperation`** - Finds matching operation from contract +3. **`withSpecValidation`** - Validates path params, query, headers, and body +4. **`withResponseHelpers`** - Attaches `respond()` method to request + +### Finally Middleware (Automatic) + +These run after your handler: + +1. **`withMissingHandler`** - Handles 404 responses +2. **`withContractFormat`** - Formats responses according to contract + +## Custom Middleware + +### Before Middleware + +Before middleware runs after validation but before your handler. Use it for: + +- Authentication +- Authorization +- Logging +- Request transformation +- Rate limiting + +```ts +const router = createRouter({ + contract, + handlers, + before: [ + // Logging middleware + async (request) => { + console.log(`${request.method} ${request.url}`); + request.startTime = Date.now(); + }, + // Authentication middleware + async (request) => { + const auth = request.headers.get("authorization"); + if (!auth) { + throw new Error("Unauthorized"); + } + // Attach user info to request + request.user = await getUserFromToken(auth); + }, + ], +}); +``` + +### Finally Middleware + +Finally middleware runs after your handler, before the response is sent. Use it for: + +- Response transformation +- CORS headers +- Response timing +- Response logging + +```ts +const router = createRouter({ + contract, + handlers, + finally: [ + // CORS middleware + async (request, response) => { + response.headers.set("access-control-allow-origin", "*"); + response.headers.set("access-control-allow-methods", "GET, POST, PUT, DELETE"); + return response; + }, + // Response timing + async (request, response) => { + if (request.startTime) { + const duration = Date.now() - request.startTime; + response.headers.set("x-response-time", `${duration}ms`); + } + return response; + }, + ], +}); +``` + +## Middleware Types + +### RequestHandler + +Before middleware uses the `RequestHandler` type: + +```ts +type RequestHandler = ( + request: RequestType, + ...args: Args +) => void | Promise | Response | Promise; +``` + +If a middleware returns a `Response`, it short-circuits the request and that response is returned immediately. + +### ResponseHandler + +Finally middleware uses the `ResponseHandler` type: + +```ts +type ResponseHandler = ( + response: unknown, + request: IRequest +) => Response | Promise; +``` + +## Common Middleware Patterns + +### Authentication Middleware + +```ts +async function withAuth(request: IRequest) { + const authHeader = request.headers.get("authorization"); + + if (!authHeader) { + throw new Error("Missing authorization header"); + } + + const token = authHeader.replace("Bearer ", ""); + const user = await verifyToken(token); + + if (!user) { + throw new Error("Invalid token"); + } + + // Attach user to request + (request as any).user = user; +} + +const router = createRouter({ + contract, + handlers, + before: [withAuth], +}); +``` + +### Logging Middleware + +```ts +async function withLogging(request: IRequest) { + const startTime = Date.now(); + (request as any).startTime = startTime; + + console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`); + + // Log response in finally middleware + return async (response: Response) => { + const duration = Date.now() - startTime; + console.log(`[${new Date().toISOString()}] ${request.method} ${request.url} ${response.status} ${duration}ms`); + return response; + }; +} + +const router = createRouter({ + contract, + handlers, + before: [withLogging], + finally: [ + async (response, request) => { + // Access startTime from request + const startTime = (request as any).startTime; + if (startTime) { + const duration = Date.now() - startTime; + response.headers.set("x-response-time", `${duration}ms`); + } + return response; + }, + ], +}); +``` + +### CORS Middleware + +```ts +function withCORS(allowedOrigins: string[] = ["*"]) { + return async (response: Response, request: IRequest) => { + const origin = request.headers.get("origin"); + + if (allowedOrigins.includes("*") || (origin && allowedOrigins.includes(origin))) { + response.headers.set("access-control-allow-origin", origin || "*"); + response.headers.set("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS"); + response.headers.set("access-control-allow-headers", "Content-Type, Authorization"); + response.headers.set("access-control-max-age", "86400"); + } + + return response; + }; +} + +const router = createRouter({ + contract, + handlers, + finally: [withCORS(["https://example.com"])], +}); +``` + +### Rate Limiting Middleware + +```ts +const rateLimiter = new Map(); + +function withRateLimit(maxRequests: number = 100, windowMs: number = 60000) { + return async (request: IRequest) => { + const key = request.headers.get("x-forwarded-for") || "unknown"; + const now = Date.now(); + + const limit = rateLimiter.get(key); + + if (limit && limit.resetAt > now) { + if (limit.count >= maxRequests) { + throw new Error("Rate limit exceeded"); + } + limit.count++; + } else { + rateLimiter.set(key, { count: 1, resetAt: now + windowMs }); + } + }; +} + +const router = createRouter({ + contract, + handlers, + before: [withRateLimit(100, 60000)], // 100 requests per minute +}); +``` + +### Request Transformation + +```ts +async function withRequestTransform(request: IRequest) { + // Transform request before handler + if (request.method === "POST" && request.headers.get("content-type")?.includes("json")) { + // Request body is already validated, but you can transform it + const body = await request.json(); + (request as any).transformedBody = { + ...body, + createdAt: new Date().toISOString(), + }; + } +} + +const router = createRouter({ + contract, + handlers, + before: [withRequestTransform], +}); +``` + +### Response Transformation + +```ts +async function withResponseTransform(response: Response, request: IRequest) { + // Transform response after handler + if (response.headers.get("content-type")?.includes("json")) { + const body = await response.json(); + const transformed = { + data: body, + meta: { + timestamp: new Date().toISOString(), + version: "1.0.0", + }, + }; + return new Response(JSON.stringify(transformed), { + status: response.status, + headers: response.headers, + }); + } + return response; +} + +const router = createRouter({ + contract, + handlers, + finally: [withResponseTransform], +}); +``` + +## Middleware Ordering + +Middleware runs in the order specified: + +```ts +const router = createRouter({ + contract, + handlers, + before: [ + middleware1, // Runs first + middleware2, // Runs second + middleware3, // Runs third + ], + finally: [ + middleware4, // Runs first (after handler) + middleware5, // Runs second + ], +}); +``` + +### Built-in Middleware Order + +Built-in middleware always runs in a specific order: + +**Before:** +1. `withParams` +2. `withMatchingContractOperation` +3. `withSpecValidation` +4. `withResponseHelpers` +5. Your custom `before` middleware + +**Finally:** +1. `withMissingHandler` +2. `withContractFormat` +3. Your custom `finally` middleware + +## Short-Circuiting + +If a before middleware returns a `Response`, it short-circuits the request: + +```ts +async function withAuth(request: IRequest) { + const auth = request.headers.get("authorization"); + if (!auth) { + // Short-circuit: return error response immediately + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { status: 401 } + ); + } + // Continue to next middleware/handler +} + +const router = createRouter({ + contract, + handlers, + before: [withAuth], // If this returns a Response, handler never runs +}); +``` + +## Error Handling in Middleware + +Errors thrown in middleware are caught by the error handler: + +```ts +async function withAuth(request: IRequest) { + const auth = request.headers.get("authorization"); + if (!auth) { + throw new Error("Unauthorized"); // Caught by error handler + } +} + +// Error handler (built-in) converts to 500 response +// Or customize in router.catch +``` + +## Best Practices + +### 1. Keep Middleware Focused + +```ts +// ✅ Good - single responsibility +async function withAuth(request: IRequest) { + // Only authentication +} + +async function withLogging(request: IRequest) { + // Only logging +} + +// ❌ Bad - mixed concerns +async function withAuthAndLogging(request: IRequest) { + // Authentication AND logging +} +``` + +### 2. Use TypeScript for Request Extension + +```ts +interface AuthenticatedRequest extends IRequest { + user: User; +} + +async function withAuth(request: AuthenticatedRequest) { + // TypeScript knows request.user exists + request.user = await getUser(); +} +``` + +### 3. Handle Errors Gracefully + +```ts +async function withAuth(request: IRequest) { + try { + const user = await getUser(); + (request as any).user = user; + } catch (error) { + // Log error but don't throw + console.error("Auth error:", error); + // Or throw to use error handler + throw error; + } +} +``` + +### 4. Reuse Middleware + +```ts +// Create reusable middleware +export const authMiddleware = async (request: IRequest) => { + // ... +}; + +export const loggingMiddleware = async (request: IRequest) => { + // ... +}; + +// Use in multiple routers +const router1 = createRouter({ + contract: contract1, + handlers: handlers1, + before: [authMiddleware, loggingMiddleware], +}); + +const router2 = createRouter({ + contract: contract2, + handlers: handlers2, + before: [authMiddleware, loggingMiddleware], +}); +``` + +## Related Topics + +- [Router Configuration](/guide/router-configuration) - Learn about router options +- [Error Handling](/guide/error-handling) - Handle errors in middleware +- [Examples](/examples/authentication) - See middleware examples + diff --git a/docs-site/guide/migration.md b/docs-site/guide/migration.md new file mode 100644 index 0000000..37b2567 --- /dev/null +++ b/docs-site/guide/migration.md @@ -0,0 +1,328 @@ +# Migration Guide + +This guide helps you migrate to `itty-spec` from other frameworks or upgrade between versions. + +## From itty-router + +If you're already using `itty-router`, migrating to `itty-spec` is straightforward. + +### Before (itty-router) + +```ts +import { Router } from "itty-router"; + +const router = Router(); + +router.get("/users/:id", async (request) => { + const { id } = request.params; + const user = await getUser(id); + return json(user); +}); +``` + +### After (itty-spec) + +```ts +import { createContract, createRouter } from "itty-spec"; +import { z } from "zod"; + +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ id: z.string().uuid() }), + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); + +const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { + const { id } = request.validatedParams; // Typed! + const user = await getUser(id); + return request.respond({ + status: 200, + contentType: "application/json", + body: user, // Type-checked! + }); + }, + }, +}); +``` + +### Key Differences + +1. **Contracts**: Define your API structure upfront +2. **Validation**: Automatic validation of all inputs +3. **Type Safety**: Full TypeScript inference +4. **Response Helpers**: Use `request.respond()` instead of `json()` + +## From Express/Fastify + +### Before (Express) + +```ts +import express from "express"; + +const app = express(); +app.use(express.json()); + +app.get("/users/:id", async (req, res) => { + const { id } = req.params; + const user = await getUser(id); + res.json(user); +}); +``` + +### After (itty-spec) + +```ts +import { createContract, createRouter } from "itty-spec"; +import { createServerAdapter } from "@whatwg-node/server"; +import { createServer } from "http"; + +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ id: z.string().uuid() }), + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); + +const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { + const { id } = request.validatedParams; + const user = await getUser(id); + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); + }, + }, +}); + +const adapter = createServerAdapter(router.fetch); +const server = createServer(adapter); +server.listen(3000); +``` + +## From Hono + +### Before (Hono) + +```ts +import { Hono } from "hono"; + +const app = new Hono(); + +app.get("/users/:id", async (c) => { + const id = c.req.param("id"); + const user = await getUser(id); + return c.json(user); +}); +``` + +### After (itty-spec) + +```ts +import { createContract, createRouter } from "itty-spec"; + +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ id: z.string().uuid() }), + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); + +const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { + const { id } = request.validatedParams; + const user = await getUser(id); + return request.respond({ + status: 200, + contentType: "application/json", + body: user, + }); + }, + }, +}); +``` + +## From tRPC + +### Before (tRPC) + +```ts +import { z } from "zod"; +import { router, publicProcedure } from "./trpc"; + +export const appRouter = router({ + getUser: publicProcedure + .input(z.object({ id: z.string().uuid() })) + .query(async ({ input }) => { + return await getUser(input.id); + }), +}); +``` + +### After (itty-spec) + +```ts +import { createContract, createRouter } from "itty-spec"; +import { z } from "zod"; + +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ id: z.string().uuid() }), + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); + +const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { + const { id } = request.validatedParams; + return request.respond({ + status: 200, + contentType: "application/json", + body: await getUser(id), + }); + }, + }, +}); +``` + +## Version Upgrades + +### From 0.1.x to 0.2.x + +#### Breaking Changes + +1. **Response Format**: Responses now use `request.respond()` instead of returning objects directly + +```ts +// Before +return { status: 200, body: user }; + +// After +return request.respond({ + status: 200, + contentType: "application/json", + body: user, +}); +``` + +2. **Request Body**: Use `request.validatedBody` instead of `request.body` + +```ts +// Before +const body = request.body; + +// After +const body = request.validatedBody; +``` + +3. **Path Parameters**: Use `request.validatedParams` instead of `request.params` + +```ts +// Before +const { id } = request.params; + +// After +const { id } = request.validatedParams; +``` + +## Migration Checklist + +- [ ] Install `itty-spec` and required dependencies +- [ ] Define contracts for all endpoints +- [ ] Convert handlers to use `request.respond()` +- [ ] Update path parameter access to `request.validatedParams` +- [ ] Update query parameter access to `request.validatedQuery` +- [ ] Update body access to `request.validatedBody` +- [ ] Update header access to `request.validatedHeaders` +- [ ] Test all endpoints +- [ ] Update error handling +- [ ] Generate OpenAPI spec +- [ ] Update deployment configuration + +## Common Migration Issues + +### Issue: Type Errors + +**Problem**: TypeScript errors after migration + +**Solution**: Ensure you're using `as const` for contracts: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + // ... + }, +} as const); +``` + +### Issue: Validation Errors + +**Problem**: Requests fail validation unexpectedly + +**Solution**: Check that your schemas match your actual data: + +```ts +// Ensure schemas match actual data +pathParams: z.object({ + id: z.string().uuid(), // Matches actual UUID format +}) +``` + +### Issue: Missing Handlers + +**Problem**: Routes return 404 + +**Solution**: Ensure all contract operations have corresponding handlers: + +```ts +const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { /* ... */ }, + // All operations must have handlers + }, +}); +``` + +## Related Topics + +- [Getting Started](/guide/getting-started) - Learn the basics +- [Contracts](/guide/contracts) - Understand contracts +- [Router Configuration](/guide/router-configuration) - Configure your router + diff --git a/docs-site/guide/openapi.md b/docs-site/guide/openapi.md new file mode 100644 index 0000000..6177194 --- /dev/null +++ b/docs-site/guide/openapi.md @@ -0,0 +1,452 @@ +# OpenAPI Integration + +`itty-spec` can automatically generate OpenAPI 3.1 specifications from your contracts, enabling automatic API documentation and tooling integration. + +## Generating OpenAPI Specifications + +Use `createOpenApiSpecification` to generate an OpenAPI spec from your contract: + +```ts +import { createOpenApiSpecification } from "itty-spec/openapi"; +import { contract } from "./contract"; + +const openApiSpec = await createOpenApiSpecification(contract, { + title: "My API", + version: "1.0.0", + description: "A comprehensive API built with itty-spec", + servers: [ + { url: "https://api.example.com", description: "Production" }, + { url: "https://staging-api.example.com", description: "Staging" }, + ], +}); +``` + +## OpenAPI Options + +The `createOpenApiSpecification` function accepts the following options: + +```ts +type OpenApiSpecificationOptions = { + title: string; // Required: API title + description?: string; // API description (supports markdown) + summary?: string; // Short summary + version?: string; // API version (default: "0.0.0") + termsOfService?: string; // Terms of service URL + contact?: { // Contact information + name?: string; + url?: string; + email?: string; + }; + license?: { // License information + identifier?: string; + name?: string; + url?: string; + }; + servers?: Array<{ // Server URLs + url: string; + description?: string; + }>; + tags?: Array<{ // Operation tags + name: string; + description?: string; + }>; +}; +``` + +### Complete Example + +```ts +const openApiSpec = await createOpenApiSpecification(contract, { + title: "User Management API", + version: "1.0.0", + description: ` +# User Management API + +This API provides endpoints for managing users. + +## Features + +- User CRUD operations +- Authentication +- Role-based access control + `, + servers: [ + { url: "https://api.example.com", description: "Production" }, + { url: "https://staging-api.example.com", description: "Staging" }, + ], + contact: { + name: "API Support", + email: "support@example.com", + url: "https://example.com/support", + }, + license: { + identifier: "MIT", + name: "MIT License", + url: "https://opensource.org/licenses/MIT", + }, + termsOfService: "https://example.com/terms", + tags: [ + { name: "Users", description: "User management endpoints" }, + { name: "Authentication", description: "Authentication endpoints" }, + ], +}); +``` + +## Serving OpenAPI Specifications + +Add the OpenAPI spec as a route in your router: + +```ts +import { createRouter } from "itty-spec"; +import { createOpenApiSpecification } from "itty-spec/openapi"; +import { z } from "zod"; + +// Generate the spec +const openApiSpec = await createOpenApiSpecification(contract, { + title: "My API", + version: "1.0.0", +}); + +// Add to contract +const extendedContract = { + ...contract, + getOpenApiSpec: { + path: "/openapi.json", + method: "GET", + responses: { + 200: { + "application/json": { body: z.any() }, + }, + }, + }, +}; + +// Add handler +const router = createRouter({ + contract: extendedContract, + handlers: { + ...yourHandlers, + getOpenApiSpec: async (request) => { + return request.respond({ + status: 200, + contentType: "application/json", + body: openApiSpec, + }); + }, + }, +}); +``` + +## Schema Deduplication + +`itty-spec` automatically deduplicates schemas in the OpenAPI spec. If the same schema is used multiple times, it's defined once in `components.schemas` and referenced elsewhere: + +```ts +// Contract uses UserSchema multiple times +const contract = createContract({ + getUser: { + // Uses UserSchema + responses: { + 200: { "application/json": { body: UserSchema } }, + }, + }, + createUser: { + // Uses UserSchema again + responses: { + 201: { "application/json": { body: UserSchema } }, + }, + }, +}); + +// OpenAPI spec defines UserSchema once in components.schemas +// Both operations reference it via $ref +``` + +## Schema References + +Schemas are automatically converted to OpenAPI format and referenced: + +```ts +// Zod schema +const UserSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + email: z.string().email(), +}); + +// OpenAPI spec +{ + "components": { + "schemas": { + "UserSchema": { + "type": "object", + "properties": { + "id": { "type": "string", "format": "uuid" }, + "name": { "type": "string" }, + "email": { "type": "string", "format": "email" } + }, + "required": ["id", "name", "email"] + } + } + } +} +``` + +## Operation Metadata + +Operation metadata from your contract is included in the OpenAPI spec: + +```ts +const contract = createContract({ + getUser: { + operationId: "getUserById", + summary: "Get user by ID", + description: "Retrieves a user by their unique identifier", + tags: ["Users"], + path: "/users/:id", + method: "GET", + responses: { /* ... */ }, + }, +}); + +// OpenAPI spec includes: +{ + "paths": { + "/users/{id}": { + "get": { + "operationId": "getUserById", + "summary": "Get user by ID", + "description": "Retrieves a user by their unique identifier", + "tags": ["Users"], + // ... + } + } + } +} +``` + +## Integration with Documentation Tools + +### Swagger UI + +Serve Swagger UI alongside your OpenAPI spec: + +```ts +const router = createRouter({ + contract: { + ...contract, + getOpenApiSpec: { + path: "/openapi.json", + method: "GET", + responses: { + 200: { "application/json": { body: z.any() } }, + }, + }, + getSwaggerUI: { + path: "/docs", + method: "GET", + responses: { + 200: { "text/html": { body: z.string() } }, + }, + }, + }, + handlers: { + ...yourHandlers, + getOpenApiSpec: async (request) => { + return request.respond({ + status: 200, + contentType: "application/json", + body: openApiSpec, + }); + }, + getSwaggerUI: async (request) => { + const html = ` + + + + API Documentation + + + +
+ + + + + `; + return request.respond({ + status: 200, + contentType: "text/html", + body: html, + }); + }, + }, +}); +``` + +### Redoc + +Similar setup for Redoc: + +```ts +getRedoc: async (request) => { + const html = ` + + + + API Documentation + + + + + + + + + `; + return request.respond({ + status: 200, + contentType: "text/html", + body: html, + }); +}, +``` + +### Elements (Stoplight) + +Elements provides a modern API documentation experience: + +```ts +getElements: async (request) => { + const html = ` + + + + API Documentation + + + + + + + + `; + return request.respond({ + status: 200, + contentType: "text/html", + body: html, + }); +}, +``` + +## Customizing OpenAPI Output + +The OpenAPI spec is generated automatically, but you can customize it by modifying the contract: + +### Adding Examples + +```ts +const UserSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + email: z.string().email(), +}).openapi({ + example: { + id: "123e4567-e89b-12d3-a456-426614174000", + name: "John Doe", + email: "john@example.com", + }, +}); +``` + +### Adding Descriptions + +```ts +const UserSchema = z.object({ + id: z.string().uuid().describe("User unique identifier"), + name: z.string().describe("User's full name"), + email: z.string().email().describe("User's email address"), +}); +``` + +## Best Practices + +### 1. Keep Specs Up to Date + +Generate the OpenAPI spec at build time or startup: + +```ts +// Generate once at startup +const openApiSpec = await createOpenApiSpecification(contract, options); + +// Serve from memory +const router = createRouter({ + contract: extendedContract, + handlers: { + getOpenApiSpec: async () => { + return request.respond({ + status: 200, + contentType: "application/json", + body: openApiSpec, + }); + }, + }, +}); +``` + +### 2. Use Descriptive Metadata + +```ts +// ✅ Good +{ + summary: "Get user by ID", + description: "Retrieves a user by their unique identifier. Returns 404 if user not found.", + tags: ["Users"], +} + +// ❌ Bad +{ + summary: "Get user", + // Missing description and tags +} +``` + +### 3. Organize with Tags + +```ts +tags: ["Users", "Public"] // Groups operations in documentation +``` + +### 4. Provide Server URLs + +```ts +servers: [ + { url: "https://api.example.com", description: "Production" }, + { url: "https://staging-api.example.com", description: "Staging" }, +] +``` + +### 5. Include Contact Information + +```ts +contact: { + name: "API Support", + email: "support@example.com", + url: "https://example.com/support", +} +``` + +## Related Topics + +- [Contracts](/guide/contracts) - Learn about contract definitions +- [Schema Libraries](/guide/schema-libraries) - Understand schema support +- [Examples](/examples/complex) - See OpenAPI integration examples + diff --git a/docs-site/guide/router-configuration.md b/docs-site/guide/router-configuration.md new file mode 100644 index 0000000..adaf4dd --- /dev/null +++ b/docs-site/guide/router-configuration.md @@ -0,0 +1,416 @@ +# Router Configuration + +The `createRouter` function is the heart of `itty-spec`. It binds your contract to handlers and configures the request/response pipeline. + +## Basic Usage + +```ts +import { createRouter } from "itty-spec"; + +const router = createRouter({ + contract, + handlers: { + // Your handlers here + }, +}); +``` + +## Complete Options Reference + +```ts +createRouter({ + contract: TContract, // Required: Your contract definition + handlers: { // Required: Handlers for each operation + [operationId: string]: HandlerFunction; + }, + base?: string, // Optional: Base path for all routes + missing?: HandlerFunction, // Optional: Handler for unmatched routes + before?: RequestHandler[], // Optional: Middleware to run before handlers + finally?: ResponseHandler[], // Optional: Middleware to run after handlers + format?: ResponseHandler, // Optional: Custom response formatter +}) +``` + +## Contract + +The `contract` option is required and defines your API operations: + +```ts +const router = createRouter({ + contract: myContract, + handlers: { /* ... */ }, +}); +``` + +## Handlers + +Handlers implement the business logic for each operation in your contract. Each handler receives a typed request object: + +```ts +const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { + // request is fully typed based on the contract + const { id } = request.validatedParams; + // ... + }, + createUser: async (request) => { + const body = request.validatedBody; + // ... + }, + }, +}); +``` + +### Handler Type Signature + +```ts +type HandlerFunction = ( + request: ContractRequest, + ...args: Args +) => Promise>; +``` + +## Base Path + +Use `base` to prefix all routes with a common path: + +```ts +const router = createRouter({ + contract, + handlers, + base: "/api/v1", // All routes are prefixed with /api/v1 +}); + +// Contract path: "/users" +// Actual route: "/api/v1/users" +``` + +This is useful for: +- API versioning +- Mounting routers at specific paths +- Organizing routes by domain + +## Missing Route Handler + +The `missing` option defines what happens when no route matches: + +```ts +const router = createRouter({ + contract, + handlers, + missing: async (request) => { + return request.respond({ + status: 404, + contentType: "application/json", + body: { + error: "Not Found", + path: new URL(request.url).pathname, + }, + }); + }, +}); +``` + +**Default**: Returns a 404 response with a JSON error message. + +## Middleware + +Middleware functions run at different stages of the request lifecycle. + +### Before Middleware + +`before` middleware runs after validation but before your handler: + +```ts +const router = createRouter({ + contract, + handlers, + before: [ + // Logging middleware + async (request) => { + console.log(`${request.method} ${request.url}`); + }, + // Authentication middleware + async (request) => { + const auth = request.headers.get("authorization"); + if (!auth) { + throw new Error("Unauthorized"); + } + }, + ], +}); +``` + +**Built-in before middleware** (runs automatically): +1. `withParams` - Extracts path parameters +2. `withMatchingContractOperation` - Finds matching operation +3. `withSpecValidation` - Validates request data +4. `withResponseHelpers` - Adds `respond()` method + +### Finally Middleware + +`finally` middleware runs after your handler, before the response is sent: + +```ts +const router = createRouter({ + contract, + handlers, + finally: [ + // Response timing middleware + async (request, response) => { + const duration = Date.now() - request.startTime; + response.headers.set("x-response-time", `${duration}ms`); + return response; + }, + // CORS middleware + async (request, response) => { + response.headers.set("access-control-allow-origin", "*"); + return response; + }, + ], +}); +``` + +**Built-in finally middleware**: +1. `withMissingHandler` - Handles 404s +2. `withContractFormat` - Formats responses + +## Custom Response Formatting + +The `format` option allows you to customize how responses are formatted: + +```ts +const router = createRouter({ + contract, + handlers, + format: async (request, response) => { + // Custom formatting logic + if (response.body && typeof response.body === 'object') { + return new Response( + JSON.stringify(response.body, null, 2), // Pretty print + { + status: response.status, + headers: response.headers, + } + ); + } + return response; + }, +}); +``` + +**Default**: Automatically formats responses based on content type and contract. + +## Additional Handler Arguments + +You can pass additional arguments to handlers using TypeScript generics: + +```ts +type Context = { + db: Database; + logger: Logger; +}; + +const router = createRouter({ + contract, + handlers: { + getUser: async (request, context) => { + // context is typed as Context + const user = await context.db.findUser(request.validatedParams.id); + context.logger.info("User retrieved", { userId: user.id }); + // ... + }, + }, +}); + +// When calling router.fetch, pass context: +router.fetch(request, { db, logger }); +``` + +This is useful for: +- Dependency injection +- Request context +- Shared services + +## Advanced Patterns + +### Multiple Routers + +Combine multiple routers for different domains: + +```ts +const userRouter = createRouter({ + contract: userContract, + handlers: userHandlers, + base: "/users", +}); + +const productRouter = createRouter({ + contract: productContract, + handlers: productHandlers, + base: "/products", +}); + +// Combine in main router +const mainRouter = Router(); +mainRouter.all("/users/*", userRouter.fetch); +mainRouter.all("/products/*", productRouter.fetch); +``` + +### Conditional Middleware + +Apply middleware conditionally: + +```ts +const router = createRouter({ + contract, + handlers, + before: [ + ...(process.env.NODE_ENV === 'development' ? [loggingMiddleware] : []), + authMiddleware, + ], +}); +``` + +### Error Handling + +Customize error handling: + +```ts +const router = createRouter({ + contract, + handlers, + // Error handling is built-in, but you can customize + // by catching errors in middleware + before: [ + async (request) => { + try { + // Your logic + } catch (error) { + // Custom error handling + throw error; // Re-throw to use default handler + } + }, + ], +}); +``` + +## Type Parameters + +`createRouter` accepts three type parameters: + +```ts +createRouter< + TContract extends ContractDefinition, // Your contract type + RequestType extends IRequest = IRequest, // Request type (default: IRequest) + Args extends any[] = any[] // Additional handler arguments +>(options) +``` + +### Custom Request Type + +Extend the request type for additional properties: + +```ts +interface AuthenticatedRequest extends IRequest { + userId: string; + userRole: string; +} + +const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { + // request.userId and request.userRole are available + const userId = request.userId; + // ... + }, + }, +}); +``` + +## Best Practices + +### 1. Organize Handlers by Domain + +```ts +// handlers/users.ts +export const userHandlers = { + getUser: async (request) => { /* ... */ }, + createUser: async (request) => { /* ... */ }, +}; + +// handlers/products.ts +export const productHandlers = { + getProduct: async (request) => { /* ... */ }, +}; + +// index.ts +const router = createRouter({ + contract, + handlers: { + ...userHandlers, + ...productHandlers, + }, +}); +``` + +### 2. Use Base Paths for Versioning + +```ts +const v1Router = createRouter({ + contract: v1Contract, + handlers: v1Handlers, + base: "/api/v1", +}); + +const v2Router = createRouter({ + contract: v2Contract, + handlers: v2Handlers, + base: "/api/v2", +}); +``` + +### 3. Keep Middleware Focused + +```ts +// ✅ Good - single responsibility +const authMiddleware = async (request) => { + // Only authentication logic +}; + +const loggingMiddleware = async (request) => { + // Only logging logic +}; + +// ❌ Bad - mixed concerns +const authAndLoggingMiddleware = async (request) => { + // Authentication AND logging +}; +``` + +### 4. Handle Missing Routes Gracefully + +```ts +missing: async (request) => { + return request.respond({ + status: 404, + contentType: "application/json", + body: { + error: "Not Found", + message: `Route ${new URL(request.url).pathname} not found`, + availableRoutes: ["/users", "/products"], + }, + }); +}, +``` + +## Related Topics + +- [Contracts](/guide/contracts) - Learn about contract definitions +- [Middleware](/guide/middleware) - Deep dive into middleware +- [Error Handling](/guide/error-handling) - Handle errors effectively +- [Validation](/guide/validation) - Understand validation flow + diff --git a/docs-site/guide/schema-libraries.md b/docs-site/guide/schema-libraries.md new file mode 100644 index 0000000..e897544 --- /dev/null +++ b/docs-site/guide/schema-libraries.md @@ -0,0 +1,387 @@ +# Schema Libraries + +`itty-spec` uses the [Standard Schema V1](https://github.com/standard-schema/spec) interface, which provides a common abstraction layer for schema validation. This means you can use any Standard Schema V1 compatible library. + +## Supported Libraries + +### Zod (v4) - Recommended + +Zod v4 is fully supported with excellent TypeScript inference and OpenAPI generation. + +#### Installation + +```bash +npm install zod@v4 +``` + +#### Basic Usage + +```ts +import { z } from "zod"; +import { createContract } from "itty-spec"; + +const UserSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(1), + email: z.string().email(), + age: z.number().min(18).optional(), +}); + +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ + id: z.string().uuid(), + }), + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); +``` + +#### Zod Features + +- Excellent TypeScript inference +- Rich validation methods +- Transform support +- Refinement support +- OpenAPI generation support + +#### Example with Transforms + +```ts +const QuerySchema = z.object({ + page: z.string().transform(Number).pipe(z.number().min(1)), + limit: z.string().transform(Number).pipe(z.number().min(1).max(100)), + tags: z.string().transform(s => s.split(',')), +}); +``` + +### Valibot + +Valibot is fully supported with OpenAPI generation via `@standard-community/standard-openapi`. + +#### Installation + +```bash +npm install valibot +``` + +#### Basic Usage + +```ts +import * as v from "valibot"; +import { createContract } from "itty-spec"; + +const UserSchema = v.object({ + id: v.pipe(v.string(), v.uuid()), + name: v.pipe(v.string(), v.minLength(1)), + email: v.pipe(v.string(), v.email()), + age: v.optional(v.pipe(v.number(), v.minValue(18))), +}); + +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: v.object({ + id: v.pipe(v.string(), v.uuid()), + }), + responses: { + 200: { + "application/json": { body: UserSchema }, + }, + }, + }, +}); +``` + +#### Valibot Features + +- Smaller bundle size than Zod +- Similar API to Zod +- OpenAPI generation support +- Good TypeScript inference + +#### Example with Pipes + +```ts +const QuerySchema = v.object({ + page: v.pipe( + v.string(), + v.transform(Number), + v.number(), + v.minValue(1) + ), + limit: v.pipe( + v.string(), + v.transform(Number), + v.number(), + v.minValue(1), + v.maxValue(100) + ), +}); +``` + +## Standard Schema V1 + +Standard Schema V1 provides a common interface that all compatible libraries implement. This ensures: + +- **Portability**: Switch between libraries without changing your contracts +- **Type Safety**: Consistent type inference across libraries +- **Runtime Validation**: Same validation behavior regardless of library + +### Standard Schema Interface + +All Standard Schema V1 compatible schemas implement: + +```ts +interface StandardSchemaV1 { + '~standard': { + validate: (data: unknown) => Promise; + }; +} +``` + +## Choosing a Schema Library + +### Use Zod if: + +- You want the best TypeScript inference +- You need extensive validation features +- You prefer a more mature ecosystem +- Bundle size is not a primary concern + +### Use Valibot if: + +- Bundle size is critical +- You want similar features to Zod +- You're building for edge/serverless environments +- You prefer a more modular approach + +## Migration Between Libraries + +Since both libraries implement Standard Schema V1, you can migrate between them: + +### From Zod to Valibot + +```ts +// Before (Zod) +import { z } from "zod"; + +const UserSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(1), + email: z.string().email(), +}); + +// After (Valibot) +import * as v from "valibot"; + +const UserSchema = v.object({ + id: v.pipe(v.string(), v.uuid()), + name: v.pipe(v.string(), v.minLength(1)), + email: v.pipe(v.string(), v.email()), +}); +``` + +### From Valibot to Zod + +```ts +// Before (Valibot) +import * as v from "valibot"; + +const UserSchema = v.object({ + id: v.pipe(v.string(), v.uuid()), + name: v.pipe(v.string(), v.minLength(1)), + email: v.pipe(v.string(), v.email()), +}); + +// After (Zod) +import { z } from "zod"; + +const UserSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(1), + email: z.string().email(), +}); +``` + +## Library-Specific Patterns + +### Zod Patterns + +#### Using Refinements + +```ts +const PasswordSchema = z.string() + .min(8) + .refine( + (val) => /[A-Z]/.test(val), + { message: "Password must contain at least one uppercase letter" } + ) + .refine( + (val) => /[0-9]/.test(val), + { message: "Password must contain at least one number" } + ); +``` + +#### Using Preprocess + +```ts +const QuerySchema = z.preprocess( + (val) => { + if (typeof val === 'string') { + return { page: val, limit: '10' }; + } + return val; + }, + z.object({ + page: z.string(), + limit: z.string(), + }) +); +``` + +### Valibot Patterns + +#### Using Pipes + +```ts +const PasswordSchema = v.pipe( + v.string(), + v.minLength(8), + v.custom((val) => /[A-Z]/.test(val), "Must contain uppercase"), + v.custom((val) => /[0-9]/.test(val), "Must contain number") +); +``` + +#### Using Transform + +```ts +const QuerySchema = v.object({ + page: v.pipe( + v.string(), + v.transform(Number), + v.number(), + v.minValue(1) + ), +}); +``` + +## OpenAPI Generation + +Both Zod and Valibot support OpenAPI generation: + +### Zod OpenAPI + +Zod schemas are automatically converted to OpenAPI: + +```ts +import { createOpenApiSpecification } from "itty-spec/openapi"; + +const spec = await createOpenApiSpecification(contract, { + title: "My API", + version: "1.0.0", +}); +``` + +### Valibot OpenAPI + +Valibot schemas are converted via `@standard-community/standard-openapi`: + +```ts +import { createOpenApiSpecification } from "itty-spec/openapi"; + +// Works the same way +const spec = await createOpenApiSpecification(contract, { + title: "My API", + version: "1.0.0", +}); +``` + +## Best Practices + +### 1. Be Consistent + +Use the same library throughout your project: + +```ts +// ✅ Good - consistent +import { z } from "zod"; +// Use Zod everywhere + +// ❌ Bad - mixed +import { z } from "zod"; +import * as v from "valibot"; +// Using both libraries +``` + +### 2. Reuse Schemas + +Define schemas once and reuse: + +```ts +// ✅ Good +const UserSchema = z.object({ /* ... */ }); + +const contract = createContract({ + getUser: { + // Uses UserSchema + responses: { 200: { "application/json": { body: UserSchema } } }, + }, + createUser: { + // Reuses UserSchema + responses: { 201: { "application/json": { body: UserSchema } } }, + }, +}); +``` + +### 3. Use Type Inference + +Let TypeScript infer types from schemas: + +```ts +// ✅ Good +const UserSchema = z.object({ + id: z.string().uuid(), + name: z.string(), +}); + +type User = z.infer; + +// ❌ Bad - manually typing +type User = { + id: string; + name: string; +}; +``` + +### 4. Validate Early + +Define schemas for all inputs: + +```ts +// ✅ Good - validates everything +const contract = createContract({ + getUser: { + path: "/users/:id", + pathParams: z.object({ id: z.string().uuid() }), + query: z.object({ include: z.array(z.string()).optional() }), + headers: z.object({ authorization: z.string() }), + responses: { /* ... */ }, + }, +}); +``` + +## Related Topics + +- [Contracts](/guide/contracts) - Learn about using schemas in contracts +- [Validation](/guide/validation) - Understand validation behavior +- [OpenAPI Integration](/guide/openapi) - Generate API documentation +- [Examples](/examples/valibot) - See Valibot examples + diff --git a/docs-site/guide/troubleshooting.md b/docs-site/guide/troubleshooting.md new file mode 100644 index 0000000..d84899e --- /dev/null +++ b/docs-site/guide/troubleshooting.md @@ -0,0 +1,339 @@ +# Troubleshooting + +Common issues and solutions when working with `itty-spec`. + +## Type Inference Issues + +### Path Parameters Not Inferred + +**Problem**: Path parameters are typed as `EmptyObject` instead of the actual parameter types. + +**Solution**: Use `as const` when creating contracts: + +```ts +// ✅ Good +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + responses: { /* ... */ }, + }, +} as const); + +// ❌ Bad - may not infer path params +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + responses: { /* ... */ }, + }, +}); +``` + +### Type Errors in Handlers + +**Problem**: TypeScript errors when accessing request properties. + +**Solution**: Ensure your handler receives the correct type: + +```ts +// ✅ Good - TypeScript infers types +const handler = async (request) => { + const { id } = request.validatedParams; // Typed correctly +}; + +// ⚠️ May need explicit type +const handler = async (request: ContractRequest) => { + const { id } = request.validatedParams; +}; +``` + +## Validation Problems + +### Validation Fails Unexpectedly + +**Problem**: Valid requests are rejected with 400 errors. + +**Solution**: Check your schemas match the actual data format: + +```ts +// Check query parameter types +query: z.object({ + page: z.string().transform(Number).pipe(z.number()), // Query params are strings + limit: z.number().default(10), // Or use defaults +}) + +// Check header normalization +headers: z.object({ + authorization: z.string(), // Headers are lowercase at runtime +}) +``` + +### Missing Content-Type Error + +**Problem**: Requests fail with "Content-Type header is required". + +**Solution**: Ensure Content-Type header is set for POST/PUT requests: + +```ts +// ✅ Good +fetch("/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name: "John" }), +}); + +// ❌ Bad - missing Content-Type +fetch("/users", { + method: "POST", + body: JSON.stringify({ name: "John" }), +}); +``` + +### Unsupported Content-Type + +**Problem**: Error "Unsupported Content-Type: ...". + +**Solution**: Ensure the Content-Type matches a defined request schema: + +```ts +// Contract defines +requests: { + "application/json": { body: UserSchema }, +} + +// Request must use +Content-Type: application/json +``` + +## Response Issues + +### Type Error on Response + +**Problem**: TypeScript error when returning response. + +**Solution**: Ensure response matches contract: + +```ts +// Contract defines +responses: { + 200: { + "application/json": { + body: z.object({ id: z.string(), name: z.string() }), + }, + }, +} + +// ✅ Good +return request.respond({ + status: 200, + contentType: "application/json", + body: { id: "123", name: "John" }, +}); + +// ❌ Bad - missing field +return request.respond({ + status: 200, + contentType: "application/json", + body: { id: "123" }, // Missing 'name' +}); +``` + +### Wrong Status Code Error + +**Problem**: TypeScript error when using status code not in contract. + +**Solution**: Only use status codes defined in contract: + +```ts +// Contract defines +responses: { + 200: { /* ... */ }, + 404: { /* ... */ }, +} + +// ✅ Good +return request.respond({ status: 200, /* ... */ }); +return request.respond({ status: 404, /* ... */ }); + +// ❌ Bad +return request.respond({ status: 500, /* ... */ }); // Not in contract +``` + +## Router Issues + +### Routes Return 404 + +**Problem**: All routes return 404. + +**Solution**: Check: +1. Contract paths match request paths +2. HTTP methods match +3. Handlers are provided for all operations + +```ts +// ✅ Good +const contract = createContract({ + getUser: { + path: "/users/:id", // Matches request + method: "GET", // Matches request + responses: { /* ... */ }, + }, +}); + +const router = createRouter({ + contract, + handlers: { + getUser: async (request) => { /* ... */ }, // Handler provided + }, +}); +``` + +### Middleware Not Running + +**Problem**: Custom middleware doesn't execute. + +**Solution**: Ensure middleware is in the correct array: + +```ts +// ✅ Good +const router = createRouter({ + contract, + handlers, + before: [myMiddleware], // Runs before handler + finally: [myMiddleware], // Runs after handler +}); +``` + +## Performance Issues + +### Slow Validation + +**Problem**: Validation is slow. + +**Solution**: +1. Use efficient schemas +2. Avoid unnecessary transforms +3. Cache validation results if possible + +```ts +// ✅ Good - efficient +const schema = z.object({ + id: z.string().uuid(), + name: z.string(), +}); + +// ❌ Bad - unnecessary transforms +const schema = z.object({ + id: z.string().transform(uuid).pipe(z.string().uuid()), + name: z.string().transform(trim).pipe(z.string()), +}); +``` + +### Large Bundle Size + +**Problem**: Bundle size is too large. + +**Solution**: +1. Use Valibot instead of Zod +2. Tree-shake unused code +3. Avoid heavy dependencies + +```ts +// ✅ Good - tree-shake +import { createContract, createRouter } from "itty-spec"; + +// ❌ Bad - imports everything +import * as ittySpec from "itty-spec"; +``` + +## OpenAPI Issues + +### OpenAPI Spec Generation Fails + +**Problem**: `createOpenApiSpecification` throws an error. + +**Solution**: Check that all schemas are Standard Schema V1 compatible: + +```ts +// ✅ Good - Standard Schema +import { z } from "zod"; +const schema = z.object({ id: z.string() }); + +// ❌ Bad - not Standard Schema +const schema = { type: "object" }; // Plain object +``` + +### Missing Schemas in OpenAPI + +**Problem**: Some schemas don't appear in OpenAPI spec. + +**Solution**: Ensure schemas are used in contract operations: + +```ts +// ✅ Good - schema used in contract +const contract = createContract({ + getUser: { + responses: { + 200: { "application/json": { body: UserSchema } }, + }, + }, +}); + +// ❌ Bad - schema defined but not used +const UserSchema = z.object({ /* ... */ }); +// Not referenced in contract +``` + +## Common Errors + +### "Validation failed" + +This means request data doesn't match the contract schema. Check: +- Path parameters format +- Query parameters types +- Header values +- Body structure + +### "Content-Type header is required" + +Set the Content-Type header for POST/PUT requests: + +```ts +headers: { + "Content-Type": "application/json", +} +``` + +### "Unsupported Content-Type" + +The Content-Type doesn't match any defined request schema. Either: +1. Add the content type to your contract +2. Use a supported content type + +### "Route not found" + +Check: +1. Path matches contract path pattern +2. HTTP method matches +3. Handler is provided for the operation + +## Getting Help + +If you're still experiencing issues: + +1. Check the [FAQ](/guide/faq) for common questions +2. Review the [Examples](/examples/) for working code +3. Open an issue on GitHub with: + - Error message + - Code snippet + - Expected vs actual behavior + +## Related Topics + +- [FAQ](/guide/faq) - Common questions +- [Validation](/guide/validation) - Understand validation +- [Type Safety](/guide/type-safety) - Type inference details + diff --git a/docs-site/guide/type-safety.md b/docs-site/guide/type-safety.md new file mode 100644 index 0000000..c113714 --- /dev/null +++ b/docs-site/guide/type-safety.md @@ -0,0 +1,466 @@ +# Type Safety + +`itty-spec` provides end-to-end type safety through TypeScript inference. Types flow automatically from your contract schemas to your handlers, ensuring compile-time guarantees about request and response shapes. + +## Type Inference Overview + +Type inference in `itty-spec` works by extracting types from your contract schemas and making them available in your handlers: + +```mermaid +flowchart LR + A[Contract Schema] --> B[TypeScript Inference] + B --> C[Handler Types] + C --> D[validatedParams] + C --> E[validatedQuery] + C --> F[validatedBody] + C --> G[validatedHeaders] + C --> H[Response Types] + + style A fill:#e1f5ff + style B fill:#fff4e1 + style C fill:#e8f5e9 +``` + +## Path Parameter Type Extraction + +Path parameters are automatically extracted from path patterns and typed. + +### Automatic Extraction + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", // Automatically typed as { id: string } + method: "GET", + responses: { /* ... */ }, + }, +}); + +// In handler: +const { id } = request.validatedParams; // Type: { id: string } +``` + +### Using `as const` for Better Inference + +For full type inference, use `as const`: + +```ts +// ✅ Good - full type inference +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + responses: { /* ... */ }, + }, +} as const); + +// TypeScript infers: { id: string } +``` + +### Explicit Path Parameter Types + +When you provide a `pathParams` schema, types are inferred from the schema: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ + id: z.string().uuid(), + }), + responses: { /* ... */ }, + }, +}); + +// Type: { id: string } (from schema output) +``` + +### Multiple Path Parameters + +```ts +const contract = createContract({ + getComment: { + path: "/posts/:postId/comments/:commentId", + method: "GET", + responses: { /* ... */ }, + }, +} as const); + +// Type: { postId: string; commentId: string } +``` + +## Query Parameter Type Inference + +Query parameter types are inferred from your query schema: + +```ts +const contract = createContract({ + searchUsers: { + path: "/users", + method: "GET", + query: z.object({ + q: z.string().min(1), + limit: z.number().min(1).max(100).default(10), + offset: z.number().optional(), + }), + responses: { /* ... */ }, + }, +}); + +// In handler: +const { q, limit, offset } = request.validatedQuery; +// Type: { q: string; limit: number; offset: number | undefined } +``` + +### Optional vs Required + +```ts +query: z.object({ + required: z.string(), // Type: string + optional: z.string().optional(), // Type: string | undefined + withDefault: z.number().default(10), // Type: number (always present) +}) +``` + +## Body Type Inference + +Body types are inferred from your request body schema: + +```ts +const contract = createContract({ + createUser: { + path: "/users", + method: "POST", + requests: { + "application/json": { + body: z.object({ + name: z.string(), + email: z.string().email(), + age: z.number().optional(), + }), + }, + }, + responses: { /* ... */ }, + }, +}); + +// In handler: +const { name, email, age } = request.validatedBody; +// Type: { name: string; email: string; age: number | undefined } +``` + +### Multiple Content Types + +When multiple content types are defined, the body type is a union: + +```ts +requests: { + "application/json": { + body: z.object({ name: z.string() }), + }, + "application/xml": { + body: z.string(), + }, +} + +// Type: { name: string } | string +``` + +## Header Type Inference + +Headers are typed based on your header schema: + +```ts +const contract = createContract({ + createUser: { + path: "/users", + method: "POST", + headers: z.object({ + authorization: z.string(), + "content-type": z.literal("application/json"), + "x-api-key": z.string(), + }), + responses: { /* ... */ }, + }, +}); + +// In handler: +const auth = request.validatedHeaders.get("authorization"); // string | null +const apiKey = request.validatedHeaders.get("x-api-key"); // string | null +``` + +### Typed Headers Interface + +Headers implement a typed interface that provides autocomplete: + +```ts +// Headers are normalized to lowercase +request.validatedHeaders.get("authorization"); // ✅ Autocomplete works +request.validatedHeaders.set("authorization", "Bearer token"); // ✅ Typed +``` + +## Response Type Checking + +Response types are checked against your contract at compile time: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + responses: { + 200: { + "application/json": { + body: z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + }), + }, + }, + 404: { + "application/json": { + body: z.object({ error: z.string() }), + }, + }, + }, + }, +}); + +// In handler: +return request.respond({ + status: 200, + contentType: "application/json", + body: { id: "123", name: "John", email: "john@example.com" }, // ✅ Valid +}); + +// TypeScript error: +return request.respond({ + status: 200, + contentType: "application/json", + body: { id: "123" }, // ❌ Missing 'name' and 'email' +}); +``` + +### Multiple Status Codes + +TypeScript ensures you can only return valid status codes: + +```ts +// ✅ Valid +return request.respond({ + status: 200, + contentType: "application/json", + body: { /* ... */ }, +}); + +// ✅ Valid +return request.respond({ + status: 404, + contentType: "application/json", + body: { error: "Not found" }, +}); + +// ❌ TypeScript error +return request.respond({ + status: 500, // Not defined in contract + contentType: "application/json", + body: { error: "Server error" }, +}); +``` + +### Multiple Content Types + +When multiple content types are defined, TypeScript ensures you use a valid one: + +```ts +responses: { + 200: { + "application/json": { body: UserSchema }, + "text/html": { body: z.string() }, + }, +} + +// ✅ Valid +return request.respond({ + status: 200, + contentType: "application/json", + body: user, +}); + +// ✅ Valid +return request.respond({ + status: 200, + contentType: "text/html", + body: "...", +}); + +// ❌ TypeScript error +return request.respond({ + status: 200, + contentType: "text/xml", // Not defined + body: "...", +}); +``` + +## Type Utilities + +`itty-spec` provides several type utilities for advanced use cases: + +### ContractOperationParameters + +Extract path parameter types: + +```ts +import type { ContractOperationParameters } from "itty-spec"; + +type Params = ContractOperationParameters; +// Type: { id: string } +``` + +### ContractOperationQuery + +Extract query parameter types: + +```ts +import type { ContractOperationQuery } from "itty-spec"; + +type Query = ContractOperationQuery; +// Type: { q: string; limit: number; offset: number | undefined } +``` + +### ContractOperationBody + +Extract body types: + +```ts +import type { ContractOperationBody } from "itty-spec"; + +type Body = ContractOperationBody; +// Type: { name: string; email: string; age: number | undefined } +``` + +### ContractOperationHeaders + +Extract header types: + +```ts +import type { ContractOperationHeaders } from "itty-spec"; + +type Headers = ContractOperationHeaders; +// Type: TypedHeaders<{ authorization: string; ... }> +``` + +### ContractOperationResponse + +Extract response types: + +```ts +import type { ContractOperationResponse } from "itty-spec"; + +type Response = ContractOperationResponse; +// Type: { status: 200; body: User } | { status: 404; body: Error } +``` + +## Advanced Type Patterns + +### Conditional Types + +Use conditional types for dynamic responses: + +```ts +type Response = + T['responses'][200] extends { 'application/json': { body: infer B } } + ? B + : never; +``` + +### Type Guards + +Create type guards for discriminated unions: + +```ts +function isSuccessResponse( + response: ContractOperationResponse +): response is { status: 200; body: User } { + return response.status === 200; +} +``` + +### Generic Handlers + +Create reusable handler types: + +```ts +type Handler = ( + request: ContractRequest +) => Promise>; +``` + +## Best Practices + +### 1. Use `as const` for Path Parameters + +```ts +// ✅ Good +const contract = createContract({ + getUser: { + path: "/users/:id", + // ... + }, +} as const); +``` + +### 2. Leverage Type Inference + +Let TypeScript infer types rather than explicitly typing: + +```ts +// ✅ Good - TypeScript infers types +const handler = async (request) => { + const { id } = request.validatedParams; + // ... +}; + +// ⚠️ Less flexible +const handler = async (request: ContractRequest) => { + // ... +}; +``` + +### 3. Use Type Utilities for Reusability + +```ts +// Extract types for reuse +type UserParams = ContractOperationParameters; +type UserQuery = ContractOperationQuery; + +// Use in utility functions +function validateUserParams(params: UserParams): boolean { + // ... +} +``` + +### 4. Leverage Discriminated Unions + +Response types are discriminated unions, use them effectively: + +```ts +const response = await request.respond({ /* ... */ }); + +if (response.status === 200) { + // TypeScript knows body is User + console.log(response.body.name); +} else if (response.status === 404) { + // TypeScript knows body is Error + console.log(response.body.error); +} +``` + +## Related Topics + +- [Contracts](/guide/contracts) - Learn about contract definitions +- [Validation](/guide/validation) - Understand validation behavior +- [Router Configuration](/guide/router-configuration) - Configure your router + diff --git a/docs-site/guide/validation.md b/docs-site/guide/validation.md new file mode 100644 index 0000000..96c5bbe --- /dev/null +++ b/docs-site/guide/validation.md @@ -0,0 +1,432 @@ +# Validation + +`itty-spec` automatically validates all incoming requests against your contract schemas before your handlers run. This ensures type safety and data integrity. + +## How Validation Works + +Validation happens automatically in the middleware chain: + +```mermaid +flowchart TD + A[Request Arrives] --> B[Extract Path Params] + B --> C[Validate Path Params] + C --> D[Parse Query String] + D --> E[Validate Query Params] + E --> F[Normalize Headers] + F --> G[Validate Headers] + G --> H[Parse Body by Content-Type] + H --> I[Validate Body] + I --> J{All Valid?} + J -->|Yes| K[Execute Handler] + J -->|No| L[Return 400 Error] +``` + +## Path Parameter Validation + +Path parameters are extracted from the URL and validated against your schema. + +### Automatic Extraction + +When you use path patterns like `/users/:id`, parameters are automatically extracted as strings: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", // id is extracted as string + method: "GET", + responses: { /* ... */ }, + }, +}); + +// In handler: +const { id } = request.validatedParams; // { id: string } +``` + +### Explicit Validation + +For stricter validation, provide a `pathParams` schema: + +```ts +const contract = createContract({ + getUser: { + path: "/users/:id", + method: "GET", + pathParams: z.object({ + id: z.string().uuid(), // Validates UUID format + }), + responses: { /* ... */ }, + }, +}); + +// Validation fails if id is not a valid UUID +// GET /users/not-a-uuid → 400 Bad Request +``` + +### Validation Errors + +Invalid path parameters return a 400 error: + +```json +{ + "error": "Validation failed", + "details": [ + { + "path": ["id"], + "message": "Invalid uuid" + } + ] +} +``` + +## Query Parameter Validation + +Query parameters are parsed from the URL query string and validated. + +### Basic Validation + +```ts +const contract = createContract({ + searchUsers: { + path: "/users", + method: "GET", + query: z.object({ + q: z.string().min(1), // Required + limit: z.number().min(1).max(100).default(10), + offset: z.number().optional(), + }), + responses: { /* ... */ }, + }, +}); +``` + +### Type Coercion + +Query parameters are strings in URLs, so you may need to transform them: + +```ts +query: z.object({ + page: z.string() + .transform(Number) + .pipe(z.number().min(1)), + limit: z.string() + .transform(Number) + .pipe(z.number().min(1).max(100)), + active: z.enum(['true', 'false']) + .transform(val => val === 'true'), +}) +``` + +### Array Parameters + +Handle array query parameters: + +```ts +// ?tags=tag1&tags=tag2 +query: z.object({ + tags: z.array(z.string()).optional(), + ids: z.array(z.string().uuid()), +}) +``` + +### Missing Parameters + +- Required parameters without defaults: validation fails +- Optional parameters: set to `undefined` +- Parameters with defaults: use default value + +## Header Validation + +Headers are normalized to lowercase and validated against your schema. + +### Basic Validation + +```ts +const contract = createContract({ + createUser: { + path: "/users", + method: "POST", + headers: z.object({ + authorization: z.string(), + "content-type": z.literal("application/json"), + "x-api-key": z.string(), + }), + responses: { /* ... */ }, + }, +}); +``` + +### Header Normalization + +All header keys are normalized to lowercase: + +```ts +// Schema definition +headers: z.object({ + "Authorization": z.string(), // Capital A + "X-API-Key": z.string(), // Mixed case +}) + +// Runtime access (always lowercase) +request.validatedHeaders.get("authorization"); // ✅ Works +request.validatedHeaders.get("x-api-key"); // ✅ Works +``` + +### Comma-Separated Values + +Some headers (like `Accept`) may contain comma-separated values. `itty-spec` handles this automatically: + +```ts +headers: z.object({ + accept: z.union([ + z.literal("application/json"), + z.literal("text/html"), + ]), +}) + +// Request: Accept: application/json, text/html +// Validation tries each value until one matches +``` + +## Body Validation + +Request bodies are validated based on the `Content-Type` header. + +### Single Content Type + +```ts +const contract = createContract({ + createUser: { + path: "/users", + method: "POST", + requests: { + "application/json": { + body: z.object({ + name: z.string().min(1), + email: z.string().email(), + }), + }, + }, + responses: { /* ... */ }, + }, +}); +``` + +### Multiple Content Types + +When multiple content types are defined, validation uses the request's `Content-Type` header: + +```ts +requests: { + "application/json": { + body: z.object({ name: z.string() }), + }, + "application/xml": { + body: z.string(), // XML as string + }, +} + +// Request with Content-Type: application/json +// → Validates against JSON schema +// Request with Content-Type: application/xml +// → Validates against XML schema +``` + +### Missing Content-Type + +If `Content-Type` is required but missing, validation fails with 400: + +```json +{ + "error": "Content-Type header is required" +} +``` + +### Unsupported Content-Type + +If the `Content-Type` doesn't match any defined schema: + +```json +{ + "error": "Unsupported Content-Type: text/plain. Supported types: application/json, application/xml" +} +``` + +### Empty Body + +If no body is sent: +- Request body is set to `{}` (empty object) +- Validation passes if body schema is optional or has defaults + +## Validation Error Handling + +When validation fails, `itty-spec` automatically returns a 400 Bad Request response. + +### Error Response Format + +```json +{ + "error": "Validation failed", + "details": [ + { + "path": ["email"], + "message": "Invalid email" + }, + { + "path": ["age"], + "message": "Expected number, received string" + } + ] +} +``` + +### Custom Error Handling + +You can customize error handling in middleware: + +```ts +const router = createRouter({ + contract, + handlers, + before: [ + async (request) => { + try { + // Validation happens automatically + } catch (error) { + if (error instanceof Error && 'issues' in error) { + // Custom validation error handling + return new Response( + JSON.stringify({ + customError: "Validation failed", + issues: error.issues, + }), + { status: 400 } + ); + } + throw error; + } + }, + ], +}); +``` + +## Custom Validation Patterns + +### Conditional Validation + +Use Zod's refinement for conditional validation: + +```ts +const schema = z.object({ + type: z.enum(['email', 'phone']), + value: z.string(), +}).refine( + (data) => { + if (data.type === 'email') { + return z.string().email().safeParse(data.value).success; + } + return /^\d{10}$/.test(data.value); + }, + { message: "Invalid value for type" } +); +``` + +### Async Validation + +For database lookups or external API calls: + +```ts +const schema = z.object({ + email: z.string().email(), +}).refine( + async (data) => { + const exists = await checkEmailExists(data.email); + return !exists; + }, + { message: "Email already exists" } +); +``` + +### Cross-Field Validation + +Validate relationships between fields: + +```ts +const schema = z.object({ + password: z.string().min(8), + confirmPassword: z.string(), +}).refine( + (data) => data.password === data.confirmPassword, + { + message: "Passwords don't match", + path: ["confirmPassword"], + } +); +``` + +## Validation Best Practices + +### 1. Validate Early, Validate Often + +Define schemas for all inputs to catch errors early: + +```ts +// ✅ Good - validates everything +const contract = createContract({ + getUser: { + path: "/users/:id", + pathParams: z.object({ id: z.string().uuid() }), + query: z.object({ include: z.array(z.string()).optional() }), + headers: z.object({ authorization: z.string() }), + responses: { /* ... */ }, + }, +}); +``` + +### 2. Use Descriptive Error Messages + +```ts +// ✅ Good +z.string().email({ message: "Please provide a valid email address" }) + +// ❌ Bad +z.string().email() +``` + +### 3. Provide Sensible Defaults + +```ts +query: z.object({ + page: z.number().min(1).default(1), + limit: z.number().min(1).max(100).default(10), + sort: z.enum(['asc', 'desc']).default('asc'), +}) +``` + +### 4. Validate Path Parameters Explicitly + +For non-string types, always provide explicit schemas: + +```ts +// ✅ Good +pathParams: z.object({ + id: z.string().uuid(), + version: z.string().transform(Number).pipe(z.number().int()), +}) +``` + +### 5. Handle Optional Fields Properly + +```ts +// ✅ Good - explicit optional +query: z.object({ + filter: z.string().optional(), + limit: z.number().default(10), // Has default, so always present +}) +``` + +## Related Topics + +- [Contracts](/guide/contracts) - Learn about defining validation schemas +- [Type Safety](/guide/type-safety) - Understand type inference from validation +- [Error Handling](/guide/error-handling) - Customize error responses + diff --git a/package.json b/package.json index 7418e53..f2c37dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "itty-spec", - "version": "0.2.6", + "version": "0.2.7", "description": "Type-safe contract first itty-router", "type": "module", "main": "./dist/index.cjs", @@ -69,12 +69,18 @@ "access": "public" }, "devDependencies": { + "@braintree/sanitize-url": "^7.1.1", "@types/bun": "^1.3.4", "@types/node": "^25.0.1", "@valibot/to-json-schema": "^1.5.0", "@vitest/coverage-v8": "^4.0.15", "@whatwg-node/server": "^0.10.17", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "dayjs": "^1.11.19", + "debug": "^4.4.3", "husky": "^9.1.7", + "mermaid": "^11.12.2", "oxfmt": "^0.17.0", "sass": "^1.97.1", "tsdown": "^0.17.0", @@ -82,6 +88,7 @@ "typescript": "^5.0.0", "valibot": "^1.2.0", "vitepress": "next", + "vitepress-plugin-mermaid": "^2.0.17", "vitest": "^4.0.15", "zod": "v4", "zod-openapi": "^4.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20af47d..57e0339 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: specifier: '=12.1.3' version: 12.1.3 devDependencies: + '@braintree/sanitize-url': + specifier: ^7.1.1 + version: 7.1.1 '@types/bun': specifier: ^1.3.4 version: 1.3.4 @@ -36,9 +39,24 @@ importers: '@whatwg-node/server': specifier: ^0.10.17 version: 0.10.17 + cytoscape: + specifier: ^3.33.1 + version: 3.33.1 + cytoscape-cose-bilkent: + specifier: ^4.1.0 + version: 4.1.0(cytoscape@3.33.1) + dayjs: + specifier: ^1.11.19 + version: 1.11.19 + debug: + specifier: ^4.4.3 + version: 4.4.3 husky: specifier: ^9.1.7 version: 9.1.7 + mermaid: + specifier: ^11.12.2 + version: 11.12.2 oxfmt: specifier: ^0.17.0 version: 0.17.0 @@ -60,6 +78,9 @@ importers: vitepress: specifier: next version: 2.0.0-alpha.15(@algolia/client-search@5.46.2)(@types/node@25.0.2)(postcss@8.5.6)(react@19.2.3)(sass@1.97.1)(search-insights@2.17.3)(tsx@4.21.0)(typescript@5.9.3) + vitepress-plugin-mermaid: + specifier: ^2.0.17 + version: 2.0.17(mermaid@11.12.2)(vitepress@2.0.0-alpha.15(@algolia/client-search@5.46.2)(@types/node@25.0.2)(postcss@8.5.6)(react@19.2.3)(sass@1.97.1)(search-insights@2.17.3)(tsx@4.21.0)(typescript@5.9.3)) vitest: specifier: ^4.0.15 version: 4.0.15(@opentelemetry/api@1.9.0)(@types/node@25.0.2)(sass@1.97.1)(tsx@4.21.0) @@ -168,6 +189,9 @@ packages: resolution: {integrity: sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==} engines: {node: '>= 14.0.0'} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@babel/generator@7.28.5': resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} @@ -193,6 +217,27 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@braintree/sanitize-url@6.0.4': + resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + + '@braintree/sanitize-url@7.1.1': + resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@docsearch/core@4.4.0': resolution: {integrity: sha512-kiwNo5KEndOnrf5Kq/e5+D9NBMCFgNsDoRpKQJ9o/xnSlheh6b8AXppMuuUVVdAUIhIfQFk/07VLjjk/fYyKmw==} peerDependencies: @@ -564,6 +609,9 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -577,6 +625,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mermaid-js/mermaid-mindmap@9.3.0': + resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==} + + '@mermaid-js/parser@0.6.3': + resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + '@napi-rs/wasm-runtime@1.1.0': resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} @@ -999,12 +1053,108 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -1026,6 +1176,9 @@ packages: '@types/node@25.0.2': resolution: {integrity: sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1202,6 +1355,11 @@ packages: resolution: {integrity: sha512-QxI+HQfJeI/UscFNCTcSri6nrHP25mtyAMbhEri7W2ctdb3EsorPuJz7IovSgNjvKVs73dg9Fmayewx1O2xOxA==} engines: {node: '>=18.0.0'} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + ai@5.0.116: resolution: {integrity: sha512-+2hYJ80/NcDWuv9K2/MLP3cTCFgwWHmHlS1tOpFUKKcmLbErAAlE/S2knsKboc3PNAu8pQkDr2N3K/Vle7ENgQ==} engines: {node: '>=18'} @@ -1257,6 +1415,14 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1264,13 +1430,189 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.1: + resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1283,6 +1625,9 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1295,6 +1640,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dts-resolver@2.1.3: resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} engines: {node: '>=20.19.0'} @@ -1363,6 +1711,9 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1390,6 +1741,10 @@ packages: engines: {node: '>=18'} hasBin: true + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + immutable@5.1.4: resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} @@ -1397,6 +1752,13 @@ packages: resolution: {integrity: sha512-roCvX171VqJ7+7pQt1kSRfwaJvFAC2zhThJWXal1rN8EqzPS3iapkAoNpHh4lM8Na1BDen+n9rVfo73RN+Y87g==} engines: {node: '>=20.19.0'} + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1443,6 +1805,29 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + katex@0.16.27: + resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + langium@3.3.1: + resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} + engines: {node: '>=16.0.0'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-es@4.17.22: + resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1464,6 +1849,9 @@ packages: mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mermaid@11.12.2: + resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==} + micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} @@ -1489,6 +1877,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1500,6 +1891,9 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -1517,6 +1911,12 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1534,6 +1934,15 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -1567,6 +1976,9 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rolldown-plugin-dts@0.18.3: resolution: {integrity: sha512-rd1LZ0Awwfyn89UndUF/HoFF4oH9a5j+2ZeuKSJYM80vmeN/p0gslYMnHTQHBEXPhUlvAlqGA3tVgXB/1qFNDg==} engines: {node: '>=20.19.0'} @@ -1596,6 +2008,15 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sass@1.97.1: resolution: {integrity: sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==} engines: {node: '>=14.0.0'} @@ -1635,6 +2056,9 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} @@ -1681,6 +2105,10 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + tsdown@0.17.3: resolution: {integrity: sha512-bgLgTog+oyadDTr9SZ57jZtb+A4aglCjo3xgJrkCDxbzcQl2l2iDDr4b06XHSQHwyDNIhYFDgPRhuu1wL3pNsw==} engines: {node: '>=20.19.0'} @@ -1719,6 +2147,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + unconfig-core@7.4.2: resolution: {integrity: sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg==} @@ -1758,6 +2189,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + valibot@1.2.0: resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: @@ -1812,6 +2247,12 @@ packages: yaml: optional: true + vitepress-plugin-mermaid@2.0.17: + resolution: {integrity: sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==} + peerDependencies: + mermaid: 10 || 11 + vitepress: ^1.0.0 || ^1.0.0-alpha + vitepress@2.0.0-alpha.15: resolution: {integrity: sha512-jhjSYd10Z6RZiKOa7jy0xMVf5NB5oSc/lS3bD/QoUc6V8PrvQR5JhC9104NEt6+oTGY/ftieVWxY9v7YI+1IjA==} hasBin: true @@ -1861,6 +2302,26 @@ packages: jsdom: optional: true + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vue@3.5.26: resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==} peerDependencies: @@ -2022,6 +2483,11 @@ snapshots: dependencies: '@algolia/client-common': 5.46.2 + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + '@babel/generator@7.28.5': dependencies: '@babel/parser': 7.28.5 @@ -2045,6 +2511,28 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@braintree/sanitize-url@6.0.4': + optional: true + + '@braintree/sanitize-url@7.1.1': {} + + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + '@docsearch/core@4.4.0(react@19.2.3)': optionalDependencies: react: 19.2.3 @@ -2263,6 +2751,12 @@ snapshots: '@iconify/types@2.0.0': {} + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2277,6 +2771,21 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mermaid-js/mermaid-mindmap@9.3.0': + dependencies: + '@braintree/sanitize-url': 6.0.4 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + khroma: 2.1.0 + non-layered-tidy-tree-layout: 2.0.2 + optional: true + + '@mermaid-js/parser@0.6.3': + dependencies: + langium: 3.3.1 + '@napi-rs/wasm-runtime@1.1.0': dependencies: '@emnapi/core': 1.7.1 @@ -2560,10 +3069,129 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} + '@types/geojson@7946.0.16': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -2587,6 +3215,9 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@3.0.3': {} '@types/web-bluetooth@0.0.21': {} @@ -2783,6 +3414,8 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.2 tslib: 2.8.1 + acorn@8.15.0: {} + ai@5.0.116(zod@4.1.13): dependencies: '@ai-sdk/gateway': 2.0.23(zod@4.1.13) @@ -2846,24 +3479,242 @@ snapshots: character-entities-legacy@3.0.0: {} + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.22 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 comma-separated-tokens@2.0.3: {} + commander@7.2.0: {} + + commander@8.3.0: {} + + confbox@0.1.8: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + csstype@3.2.3: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.1 + + cytoscape-fcose@2.2.0(cytoscape@3.33.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.1 + + cytoscape@3.33.1: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.13: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.22 + + dayjs@1.11.19: {} + debug@4.4.3: dependencies: ms: 2.1.3 defu@6.1.4: {} + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + dequal@2.0.3: {} detect-libc@1.0.3: @@ -2873,6 +3724,10 @@ snapshots: dependencies: dequal: 2.0.3 + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dts-resolver@2.1.3: {} empathic@2.0.0: {} @@ -2969,6 +3824,8 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + hachure-fill@0.5.2: {} + has-flag@4.0.0: {} hast-util-to-html@9.0.5: @@ -2999,10 +3856,18 @@ snapshots: husky@9.1.7: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + immutable@5.1.4: {} import-without-cache@0.2.3: {} + internmap@1.0.1: {} + + internmap@2.0.3: {} + is-extglob@2.1.1: optional: true @@ -3045,6 +3910,28 @@ snapshots: json-schema@0.4.0: {} + katex@0.16.27: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + langium@3.3.1: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + lodash-es@4.17.21: {} + + lodash-es@4.17.22: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3075,6 +3962,29 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 + mermaid@11.12.2: + dependencies: + '@braintree/sanitize-url': 7.1.1 + '@iconify/utils': 3.1.0 + '@mermaid-js/parser': 0.6.3 + '@types/d3': 7.4.3 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.13 + dayjs: 1.11.19 + dompurify: 3.3.1 + katex: 0.16.27 + khroma: 2.1.0 + lodash-es: 4.17.22 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 @@ -3102,6 +4012,13 @@ snapshots: mitt@3.0.1: {} + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + ms@2.1.3: {} nanoid@3.3.11: {} @@ -3109,6 +4026,9 @@ snapshots: node-addon-api@7.1.1: optional: true + non-layered-tidy-tree-layout@2.0.2: + optional: true + obug@2.1.1: {} oniguruma-parser@0.12.1: {} @@ -3132,6 +4052,10 @@ snapshots: '@oxfmt/win32-arm64': 0.17.0 '@oxfmt/win32-x64': 0.17.0 + package-manager-detector@1.6.0: {} + + path-data-parser@0.1.0: {} + pathe@2.0.3: {} perfect-debounce@2.0.0: {} @@ -3143,6 +4067,19 @@ snapshots: picomatch@4.0.3: {} + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -3171,6 +4108,8 @@ snapshots: rfdc@1.4.1: {} + robust-predicates@3.0.2: {} + rolldown-plugin-dts@0.18.3(rolldown@1.0.0-beta.53)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.5 @@ -3235,6 +4174,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + sass@1.97.1: dependencies: chokidar: 4.0.3 @@ -3275,6 +4225,8 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + stylis@4.3.6: {} + superjson@2.2.6: dependencies: copy-anything: 4.0.5 @@ -3313,6 +4265,8 @@ snapshots: trim-lines@3.0.1: {} + ts-dedent@2.2.0: {} + tsdown@0.17.3(typescript@5.9.3): dependencies: ansis: 4.2.0 @@ -3350,6 +4304,8 @@ snapshots: typescript@5.9.3: {} + ufo@1.6.1: {} + unconfig-core@7.4.2: dependencies: '@quansync/fs': 1.0.0 @@ -3390,6 +4346,8 @@ snapshots: dependencies: react: 19.2.3 + uuid@11.1.0: {} + valibot@1.2.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -3418,6 +4376,13 @@ snapshots: sass: 1.97.1 tsx: 4.21.0 + vitepress-plugin-mermaid@2.0.17(mermaid@11.12.2)(vitepress@2.0.0-alpha.15(@algolia/client-search@5.46.2)(@types/node@25.0.2)(postcss@8.5.6)(react@19.2.3)(sass@1.97.1)(search-insights@2.17.3)(tsx@4.21.0)(typescript@5.9.3)): + dependencies: + mermaid: 11.12.2 + vitepress: 2.0.0-alpha.15(@algolia/client-search@5.46.2)(@types/node@25.0.2)(postcss@8.5.6)(react@19.2.3)(sass@1.97.1)(search-insights@2.17.3)(tsx@4.21.0)(typescript@5.9.3) + optionalDependencies: + '@mermaid-js/mermaid-mindmap': 9.3.0 + vitepress@2.0.0-alpha.15(@algolia/client-search@5.46.2)(@types/node@25.0.2)(postcss@8.5.6)(react@19.2.3)(sass@1.97.1)(search-insights@2.17.3)(tsx@4.21.0)(typescript@5.9.3): dependencies: '@docsearch/css': 4.4.0 @@ -3508,6 +4473,23 @@ snapshots: - tsx - yaml + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + vue@3.5.26(typescript@5.9.3): dependencies: '@vue/compiler-dom': 3.5.26