@vielzeug/codex 26.10.7 → 26.10.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +73 -103
- package/data/llms-full.txt +756 -128
- package/data/llms.txt +3 -3
- package/data/manifest.json +1 -1
- package/data/packages/archive.json +3 -3
- package/data/packages/arsenal.json +19 -5
- package/data/packages/assay.json +6 -4
- package/data/packages/courier.json +26 -17
- package/data/packages/ore.json +10 -7
- package/data/packages/refine.json +3 -3
- package/data/packages/ripple.json +24 -29
- package/data/packages/sourcerer.json +42 -26
- package/data/refine.json +200 -240
- package/data/search.json +66 -46
- package/package.json +2 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "export {
|
|
2
|
+
"apiSource": "export { type Courier, type CourierEvent, type CourierOptions, createCourier } from './courier.js';\nexport {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';\nexport { withBearerAuth, withLogging, withRequestId } from './interceptors.js';\nexport { isProblemMediaType, PROBLEM_JSON_MEDIA_TYPE, type ProblemDetail } from './problem.js';\nexport type { ResponseType } from './response.js';\nexport type { FetchContext, Middleware, TransportOptions } from './transport.js';\nexport type {\n CourierCacheKey,\n CourierCacheKeyAtom,\n CourierCacheOptions,\n CourierReadCache,\n GetRequestConfig,\n Params,\n PrefetchConfig,\n RequestConfig,\n} from './url.js';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: 'Courier: HTTP client'\ndescription: A framework-neutral HTTP client with explicit cached reads, prefetching, immutable middleware, typed paths, and structured errors.\npackage: courier\ncategory: http\nkeywords: [http-client, fetch, cache, prefetch, middleware,
|
|
5
|
-
"api": "---\ntitle: 'Courier: API Reference'\ndescription: Reference for Courier HTTP, cached read, prefetch, middleware, and error APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createCourier()` | Creates transport client | Sync | Dispose only when whole scope ends |\n| `request()` | Sends one HTTP request of any method | Async | Direct calls never deduplicate |\n| `get()` | Sends a GET, optionally through the explicit cache | Async | One key must identify one representation |\n| `prefetch()` | Warms a cached GET | Async | Request failures resolve after surfacing through `tap()` |\n| `invalidateCache()` / `clearCache()` | Removes cached values | Sync | Invalidation matches structured key prefixes |\n| `post()` / `put()` / `patch()` / `delete()` | Common write method conveniences | Async | Use `request()` for custom methods |\n| `withBearerAuth()` | Adds authorization middleware | Sync | Token provider runs per request |\n| `withRequestId()` | Adds request identifier middleware | Sync | Default generator uses `crypto.randomUUID()` |\n| `withLogging()` | Logs request result metadata | Sync | Requires explicit logger; URLs may contain sensitive query values |\n| `tap()` | Observe structured transport events | Sync | Observation only; optional signal owns lifetime |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/courier` | Client factory, errors, middleware helpers, and public types |\n\n## Client\n\n### `createCourier()`\n\n```ts\ncreateCourier(options?: CourierOptions): Courier;\n```\n\nReturns an HTTP client sharing base URL, default headers, immutable middleware, optional parsed-value cache, timeout, and cancellation lifecycle.\n\n| `CourierOptions` field | Type | Default | Description |\n| --- | --- | --- | --- |\n| `baseUrl` | `string` | `''` | Prefix for relative request paths |\n| `cache.capacity` | `number` | `100` | Maximum cached entries; positive integer or `Infinity` |\n| `cache.ttlMs` | `number` | `30_000` | Default cache freshness; non-negative milliseconds or `Infinity` |\n| `fetch` | `typeof globalThis.fetch` | `globalThis.fetch` | Fetch implementation |\n| `headers` | `HeadersInit` | `{}` | Default request headers (immutable; merged lowercase) |\n| `middleware` | `readonly Middleware[]` | `[]` | Immutable middleware chain configured at construction |\n| `timeout` | `number` | `30_000` | Integer milliseconds from 1 to 2,147,483,647, or `Infinity` |\n\n**Returns:** `Courier`.\n\n```ts\nimport { createCourier, withBearerAuth } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withBearerAuth('token')],\n});\n```\n\n| `Courier` member | Signature | Description |\n| --- | --- | --- |\n| `request` | `<T, P>(path, config?) => Promise<T>` | Sends one HTTP request; `config.method` defaults to `GET` |\n| `get` | `<T, P>(path, config?) => Promise<T>` | Sends GET; `cache` opts parsed responses into bounded caching |\n| `prefetch` | `<T, P>(path, config) => Promise<void>` | Warms one required structured cache key; failures are tap-observed |\n| `invalidateCache` | `(prefix: CourierCacheKey) => void` | Invalidates matching settled and pending key prefixes |\n| `clearCache` | `() => void` | Removes every cached and pending entry |\n| `post` | `<T, P>(path, config?) => Promise<T>` | Sends POST |\n| `put` | `<T, P>(path, config?) => Promise<T>` | Sends PUT |\n| `patch` | `<T, P>(path, config?) => Promise<T>` | Sends PATCH |\n| `delete` | `<T, P>(path, config?) => Promise<T>` | Sends DELETE |\n| `tap` | `(handler, options?: { signal?: AbortSignal }) => () => void` | Observes structured request and disposal events |\n| `cancelAll` | `() => void` | Aborts active requests; client remains usable |\n| `dispose` | `() => void` | Final teardown; aborts work and marks client unusable |\n| `disposed` | `boolean` | Whether final disposal occurred |\n| `disposalSignal` | `AbortSignal` | Aborts on final disposal |\n\n---\n\n## Requests\n\n### `request()`\n\n```ts\nrequest<T, P extends string>(path: P, config?: RequestConfig<P, T> & { method?: string }): Promise<T>;\n```\n\nSends one HTTP request. `config.method` defaults to `GET`; pass any method (including custom verbs) to support it consistently.\n\n| `RequestConfig<P, T>` field | Type | Description |\n| --- | --- | --- |\n| `method` | `string` | HTTP method; defaults to `GET` |\n| `params` | `Record<string, string \\| number \\| boolean>` | Path parameters for `{param}` placeholders |\n| `query` | `Params` | Query string parameters |\n| `body` | `unknown` | BodyInit values pass through; streams use Node duplex; GET/HEAD reject bodies; other values encode as JSON |\n| `headers` | `HeadersInit` | Per-request headers merged with (and overriding) defaults |\n| `responseType` | `ResponseType` | Response parsing strategy |\n| `schema` | `{ parse(data: unknown): T }` | Optional parsed-response validator; incompatible with `responseType: 'raw'` |\n| `signal` | `AbortSignal` | External abort signal merged with internal cancellation |\n| `timeout` | `number` | Integer milliseconds from 1 to 2,147,483,647, or `Infinity`; overrides default |\n| `fetchInit` | `Omit<RequestInit, 'body' \\| 'headers' \\| 'method' \\| 'signal'>` | Raw fetch options for advanced use |\n\n**Returns:** Parsed response body (or `undefined` for empty bodies). With\n`responseType: 'raw'` the resolved value is the `Response` itself; a `204`/`205`\nanswer whose transport carried an unexpected empty body stream is normalized to\n`body: null` — the stray stream is cancelled and headers, status, statusText,\nURL, redirect and type metadata are preserved (on the response and its clones).\nA null-body raw response owns no pending resource, so no consumption is required\nto release the request.\n\n```ts\nconst created = await courier.request<User>('/users', {\n method: 'POST',\n body: { name: 'Ada' },\n});\n```\n\n### `get()`\n\n```ts\nget<T, P extends string>(path: P, config?: GetRequestConfig<P, T>): Promise<T>;\n```\n\nConvenience for `request(path, { ...config, method: 'GET' })`. A `cache` descriptor opts a parsed response into the bounded cache. It requires a structured key and may override the default TTL. Raw responses cannot be cached.\n\n```ts\nconst user = await courier.get<User>('/users/{id}', {\n cache: { key: ['users', 1], ttlMs: 10_000 },\n params: { id: 1 },\n});\n```\n\n## Cache\n\n### `prefetch()`\n\n```ts\nprefetch<T, P extends string>(path: P, config: PrefetchConfig<P, T>): Promise<void>;\n```\n\nStarts the same cached GET used by `get()`. `config.cache` is required; `signal`, per-call `timeout`, and `responseType: 'raw'` are unavailable. Fresh values and pending loads are reused. The promise settles when the attempt finishes. Request failures emit `request-error` through `tap()` and resolve without caching data.\n\n### `invalidateCache()` and `clearCache()`\n\n```ts\ninvalidateCache(prefix: CourierCacheKey): void;\nclearCache(): void;\n```\n\n`invalidateCache()` removes every settled entry whose structured key starts with `prefix` and prevents matching pending loads from being stored. `clearCache()` applies that behavior to all entries. Existing waiters may still receive an invalidated in-flight result; `cancelAll()` or `dispose()` aborts physical work.\n\n---\n\n## Middleware\n\nMiddleware is an immutable chain configured at construction. Each middleware receives an immutable `FetchContext` and a `next` function.\n\n```ts\ntype Middleware = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;\n```\n\n### Middleware helpers\n\n```ts\nwithBearerAuth(token: string | (() => string | null | undefined | Promise<string | null | undefined>)): Middleware;\nwithRequestId(options?: { generate?: () => string; header?: string }): Middleware;\nwithLogging(options: {\n logger: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;\n}): Middleware;\n```\n\nEach helper returns a `Middleware` passed in the `middleware` option. `withLogging` requires an explicit `logger` function, isolates logger failures from requests, and has no default console output.\n\n```ts\nconst courier = createCourier({\n middleware: [\n withBearerAuth(() => tokenStore.getAccessToken()),\n withRequestId(),\n withLogging({ logger: (msg) => console.log(msg) }),\n ],\n});\n```\n\n## Types\n\n```ts\ntype TransportOptions = {\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n headers?: HeadersInit;\n middleware?: readonly Middleware[];\n timeout?: number;\n};\n\ntype CourierCacheKeyAtom = string | number | boolean | null;\ntype CourierCacheKey = readonly [CourierCacheKeyAtom, ...CourierCacheKeyAtom[]];\n\ntype CourierCacheOptions = {\n capacity?: number;\n ttlMs?: number;\n};\n\ntype CourierReadCache = {\n key: CourierCacheKey;\n ttlMs?: number;\n};\n\ntype CourierOptions = TransportOptions & {\n cache?: CourierCacheOptions;\n};\n\ntype FetchContext = {\n readonly headers: Readonly<Record<string, string>>;\n readonly init: Readonly<Omit<RequestInit, 'headers'>>;\n readonly url: string;\n withHeaders(updates: Record<string, string | undefined>): FetchContext;\n withInit(updates: Partial<Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>>): FetchContext;\n};\n\ntype Middleware = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;\n\ntype CourierEvent =\n | { type: 'request-start'; method: string; url: string }\n | { type: 'request-success'; method: string; url: string; status: number; duration: number }\n | { type: 'request-error'; method: string; url: string; error: unknown }\n | { type: 'dispose' };\n```\n\n```ts\ntype ParamValue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;\ntype Params = Record<string, ParamValue>;\ntype ResponseType = 'auto' | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'raw';\ntype RequestConfig<P extends string = string, T = unknown> = {\n body?: unknown;\n method?: string;\n fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: HeadersInit;\n params?: Record<string, string | number | boolean>;\n query?: Params;\n responseType?: ResponseType;\n schema?: { parse(data: unknown): T };\n signal?: AbortSignal;\n timeout?: number;\n};\n\ntype GetRequestConfig<P extends string = string, T = unknown> = Omit<\n RequestConfig<P, T>,\n 'body' | 'method' | 'responseType'\n> &\n ({ cache?: CourierReadCache; responseType?: Exclude<ResponseType, 'raw'> } | { cache?: never; responseType: 'raw' }) & {\n body?: never;\n };\n\ntype PrefetchConfig<P extends string = string, T = unknown> = Omit<\n GetRequestConfig<P, T>,\n 'cache' | 'responseType' | 'signal' | 'timeout'\n> & {\n cache: CourierReadCache;\n responseType?: Exclude<ResponseType, 'raw'>;\n signal?: never;\n timeout?: never;\n};\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `CourierError` | Base class for all Courier errors | Use `instanceof` to narrow |\n| `CourierHttpError` | Non-2xx HTTP response | `status`, `data`, `headers`, `method`, `url`; `CourierHttpError.is(e, status?)` narrows by status |\n| `CourierNetworkError` | Request failure without response | `method`, `url`, `cause` |\n| `CourierTimeoutError` | Timeout signal aborts request | `method`, `url`, `cause` |\n| `CourierAbortError` | Caller or client cancellation | `method`, `url`, `cause` |\n| `CourierSchemaValidationError` | Response schema fails | `data`, `cause` |\n| `CourierParseError` | Response body cannot parse | N/A |\n| `CourierConfigError` | Invalid request config (GET/HEAD body, raw + schema) | N/A |\n| `CourierDisposedError` | Work starts after disposal | N/A |\n\nErrors thrown directly by custom middleware are preserved rather than reclassified.\n",
|
|
6
|
-
"usage": "---\ntitle: 'Courier: Usage Guide'\ndescription: Use one Courier client for typed HTTP, explicit cached reads, prefetching, immutable middleware, and structured errors.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one Courier client for an application or request scope. Its transport policy, middleware, and disposal lifecycle apply to every request.\n\n```ts\nimport { createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\nconst user = await courier.get<User>('/users/{id}', { params: { id: 1 } });\nconsole.log(user.name);\n```\n\n## HTTP Requests\n\nUse `get()`, `post()`, `put()`, `patch()`, and `delete()` for standard methods. Use `request()` when the method is custom or selected dynamically. Courier preserves root-relative paths, encodes path parameters, serializes JSON-compatible bodies, and parses successful responses.\n\n```ts\nconst posts = await courier.get<{ id: number; title: string }[]>('/users/{id}/posts', {\n params: { id: 1 },\n query: { limit: 20, status: 'published' },\n});\n\nawait courier.patch('/posts/{id}', {\n body: { title: 'Updated title' },\n params: { id: posts[0].id },\n});\n```\n\n`get()`, `post()`, `put()`, `patch()`, and `delete()` cover common methods. Use `request()` for custom verbs or when a method is selected dynamically.\n\n## Default Headers\n\nSet default headers at construction. They are merged (lowercased) into every request and cannot be mutated at runtime: middleware handles dynamic headers.\n\n```ts\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n headers: { 'x-client': 'web' },\n});\n```\n\n## Middleware\n\nMiddleware is an immutable chain configured once at construction. Each middleware receives an immutable `FetchContext` and a `next` function. Use `ctx.withHeaders()` or `ctx.withInit()` to derive a new context: never mutate `ctx` directly.\n\n```ts\nimport { withBearerAuth, withRequestId, createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [\n withBearerAuth(() => sessionStorage.getItem('access-token') ?? ''),\n withRequestId(),\n ],\n});\n```\n\nA bearer token provider may return `null`, `undefined`, or an empty string to omit a stale authorization header. Use `withLogging()` with an explicit `logger` function to log requests during local development. `withLogging()` includes full URLs, so sanitize query values before persistent logging.\n\n```ts\nimport { withLogging, createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withLogging({ logger: (msg) => console.log(msg) })],\n});\n```\n\nUse `tap()` for multiple structured transport observers. It emits request-start, request-success, request-error, and dispose events; observer failures cannot alter requests. Pass `{ signal }` to own listener lifetime.\n\n```ts\nconst stop = courier.tap((event) => metrics.record(event), { signal: routeSignal });\n```\n\nBecause middleware is immutable, a client's behavior is locally understandable from its construction call. To change policy, create a new client.\n\n## Cached Reads and Prefetching\n\nOpt parsed GETs into caching with a structured key. Courier uses a finite capacity of 100 entries and a 30-second TTL by default; client options change those defaults, and each read may override TTL. Successful parsed and schema-validated values are cached, while errors are not.\n\n```ts\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n cache: { capacity: 200, ttlMs: 60_000 },\n});\nconst cache = { key: ['accounts', accountId, 'users', userId] as const, ttlMs: 10_000 };\n\nawait courier.prefetch<User>('/users/{id}', { cache, params: { id: userId } });\nconst user = await courier.get<User>('/users/{id}', { cache, params: { id: userId } });\n```\n\n`prefetch()` returns a promise that settles when warming finishes. Request failures are not cached or rejected from that promise; the physical request emits `request-error` through `tap()`. Cache, key, and URL configuration errors still throw before prefetch starts.\n\nInvalidate a structured prefix after writes. Every matching settled or pending key becomes ineligible for caching, while unrelated entries remain available.\n\n```ts\nawait courier.patch('/users/{id}', { body: update, params: { id: userId } });\ncourier.invalidateCache(['accounts', accountId, 'users']);\n```\n\nA cache key must identify one response representation, including a stable non-secret principal or tenant identifier, schema, and relevant request variants. Never place credentials in a key; call `clearCache()` when identity changes. Cached objects are shared by reference.\n\nConcurrent callers share physical work but own their own signal and timeout. One cancelled caller does not affect the others. When every caller cancels and no prefetch owns the load, Courier aborts the physical request. `cancelAll()` always aborts physical work; `dispose()` also clears settled cache data.\n\n## Response Parsing\n\nCourier parses successful 2xx response bodies automatically based on `content-type`, unless you set `responseType` explicitly (`'json'`, `'text'`, `'blob'`, `'arrayBuffer'`, or `'raw'` for the `Response` itself). Empty JSON bodies return `undefined` instead of throwing. Raw response bodies remain owned by Courier until consumed or cancelled, so `cancelAll()` and `dispose()` still abort them. Raw responses cannot be combined with `schema`. Always consume or cancel the body to release request ownership, and set `timeout: Infinity` for intentionally long-lived streams. Courier preserves URL, redirect, and type metadata on tracked responses and their clones. A `204`/`205` raw answer is normalized to `body: null` — a stray empty transport stream is cancelled for you — so a null-body raw response never carries pending body ownership and needs no consumption.\n\n```ts\nconst buffer = await courier.request('/files/{id}', {\n method: 'GET',\n params: { id: 1 },\n responseType: 'arrayBuffer',\n});\n```\n\n## Schema Validation\n\nPass a `schema` with a `parse(data)` method to validate the parsed body. Any validator works: a `@vielzeug/spell` schema or a plain function wrapper. A failed parse throws `CourierSchemaValidationError`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst user = await courier.get('/users/1', {\n schema: s.object({ id: s.number(), name: s.string() }),\n});\n```\n\n## Cancellation and Disposal\n\nPass a `signal` to cancel one request. `cancelAll()` aborts every active request without disposing the client. `dispose()` aborts active work and marks the client unusable: call it only at the final application or request boundary.\n\n```ts\nconst controller = new AbortController();\nvoid courier.get('/slow', { signal: controller.signal });\ncontroller.abort(); // cancels that request only\n\ncourier.cancelAll(); // cancels all active requests\ncourier.dispose(); // final teardown\n```\n\n## Timeouts\n\nSet a client-wide `timeout` (default 30,000 ms) or a per-request timeout. Finite values must be integer milliseconds from 1 through 2,147,483,647. A timed-out request rejects with `CourierTimeoutError`; use `Infinity` for intentionally long-lived work.\n\n```ts\nconst courier = createCourier({ timeout: 10_000 });\nawait courier.get('/health', { timeout: 2_000 });\n```\n\n## Error Handling\n\nCourier classifies URL, request-construction, fetch, cancellation, HTTP, response-parsing, and schema failures into distinct error classes. Errors thrown directly by custom middleware are preserved.\n\n```ts\nimport {\n CourierAbortError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierTimeoutError,\n createCourier,\n} from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\n\nasync function loadUser(): Promise<string> {\n try {\n return (await courier.get<{ name: string }>('/users/1', { timeout: 2_000 })).name;\n } catch (error) {\n if (error instanceof CourierAbortError) return 'Cancelled';\n if (error instanceof CourierTimeoutError) return 'Timed out; retry.';\n if (CourierHttpError.is(error, 404)) return 'User not found.';\n if (error instanceof CourierNetworkError) return 'Check your connection.';\n if (error instanceof CourierParseError) return 'The server returned an unreadable response.';\n throw error;\n }\n}\n```\n\n- A `CourierHttpError` has a response (`status`, `data`, `headers`); a `CourierNetworkError` does not.\n- Do not display cancellation as an application error during navigation.\n- `CourierSchemaValidationError` carries the raw pre-validation `data`.\n\n## Composing with a State Layer\n\nCourier owns HTTP transport and optional parsed GET caching. Observable query state, optimistic mutations, collection navigation, and stream state belong to the owning state layer (e.g. Sourcerer, TanStack Query, or consumer code). Compose Courier calls into that layer instead of nesting state machines.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst users = createPageSource<User>({\n load: async ({ page, pageSize, signal }) => {\n const result = await courier.get<{ data: User[]; total: number }>('/users', {\n query: { page, pageSize },\n signal,\n });\n return { items: result.data, totalItems: result.total };\n },\n});\n\nawait users.reload();\n```\n\n## Framework Integration\n\nCreate Courier at an application or route boundary. Inject the client where requests are issued; let the framework own rendering and cache state.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useContext } from 'react';\nimport { createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\ntype CourierClient = ReturnType<typeof createCourier>;\nconst CourierContext = createContext<CourierClient | null>(null);\n\nexport function CourierProvider({ children, courier }: { children: React.ReactNode; courier: CourierClient }) {\n return <CourierContext.Provider value={courier}>{children}</CourierContext.Provider>;\n}\n\nexport function useCourier() {\n const courier = useContext(CourierContext);\n if (!courier) throw new Error('CourierProvider missing');\n return courier;\n}\n\n// In a component:\n// const user = await useCourier().get<User>('/users/1');\n```\n\n```ts [Vue 3]\nimport { createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: '/api' });\n// Provide via app.provide('courier', courier) and inject in setup().\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createCourier } from '@vielzeug/courier';\n export let courier: ReturnType<typeof createCourier>;\n</script>\n```\n\n:::\n\nCourier exposes no loading, error, or cache store. Construct the client at the application or SSR-request boundary, pass it into framework providers, and dispose it at that same boundary. Render state in the framework that owns the view.\n\n## Gotchas\n\n### Root-relative paths stay root-relative\n\nWith no `baseUrl`, `'/api/users'` is passed to fetch unchanged while `'api/users'` remains document-relative. With a `baseUrl`, Courier appends either spelling to that base.\n\n### `baseUrl` should not include query parameters\n\n`buildUrl` joins `baseUrl` and path with `/`. A base URL like `https://api.example.com?token=abc` produces broken URLs (`https://api.example.com?token=abc/users`). Pass query parameters per-request via `config.query` instead.\n\n### Query parameters precede fragments\n\nWhen a path already contains `#fragment`, Courier inserts `config.query` before the fragment so values remain part of the HTTP query.\n\n### GET and HEAD bodies are rejected\n\nFetch does not support bodies for GET or HEAD. Courier rejects them as request parse errors before dispatch.\n\n### Middleware is immutable\n\nThere is no `use()` method. To change middleware, create a new client. This keeps each client's behavior locally understandable and avoids runtime mutation of the request chain.\n\n### Keep credentials out of URLs when using logging middleware\n\n`withLogging()` logs the full URL, including query parameters. If URLs may contain sensitive data, provide a custom `logger` that sanitizes the URL first.\n\n## Best Practices\n\n- Create one Courier client per application or SSR request scope.\n- Configure middleware once at construction; never mutate it at runtime.\n- Use method conveniences for GET/POST/PUT/PATCH/DELETE and `request()` for custom verbs.\n- Use explicit cache keys only for parsed GETs that should share data and in-flight work.\n- Clear cached data when the authenticated principal or tenant changes.\n- Let the owning state layer own observable queries and mutations.\n- Dispose only at the final application or request boundary.\n- Keep credentials out of URLs when using logging middleware.\n",
|
|
4
|
+
"index": "---\ntitle: 'Courier: HTTP client'\ndescription: A framework-neutral HTTP client with explicit cached reads, prefetching, immutable middleware, typed paths, and structured errors.\npackage: courier\ncategory: http\nkeywords: [http-client, fetch, cache, prefetch, middleware, errors, rfc9457, problem-details]\nrelated: [flux, spell, postmaster]\nexports: [createCourier, CourierHttpError, ProblemDetail, problemDetailError, withBearerAuth]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"courier\" />\n\n## Why Courier?\n\nNative `fetch` leaves request policy, error classification, middleware composition, and repeated-read coordination to each application. Courier keeps those concerns in one client, including explicit cached GETs and warm-cache prefetching, while observable query and mutation state remain consumer-owned.\n\n```ts\n// Before\nconst response = await fetch(`/api/users/${userId}`);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst user = await response.json();\n\n// After\nconst user = await courier.get<User>('/users/{id}', { params: { id: userId } });\n```\n\n| Feature | Courier | ky | ofetch |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"courier\" type=\"size\" /> | Separate package | Separate package |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Native fetch transport | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Explicit parsed-value cache and prefetch | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Immutable middleware at construction | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Structured error taxonomy | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Courier when** one client should own typed HTTP, explicit cached GETs and prefetching, immutable middleware, and structured errors, while a separate state layer owns observable queries and mutations.\n\n**Consider ky when** you only need a compact fetch wrapper. **Consider ofetch when** you want a bundled fetch utility with its own retry and caching conventions.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/courier\n```\n\n```sh [npm]\nnpm install @vielzeug/courier\n```\n\n```sh [yarn]\nyarn add @vielzeug/courier\n```\n\n:::\n\n## Quick Start\n\nCreate one transport client for an application or request scope, then use method conveniences for standard HTTP methods and `request()` for custom methods.\n\n```ts\nimport { CourierHttpError, createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\n\ntry {\n const user = await courier.get<User>('/users/{id}', { params: { id: 42 } });\n console.log(user.name);\n} catch (error) {\n if (CourierHttpError.is(error, 404)) console.log('User not found');\n else throw error;\n} finally {\n courier.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`createCourier()`**: one lifecycle, immutable middleware pipeline, header defaults, and cancellation boundary.\n- **`request()`**: one contract for every HTTP method; pass `method` in the config.\n- **Cached `get()`**: opt in with a structured key and optional per-read TTL; successful parsed values share bounded storage and in-flight work.\n- **`prefetch()`**: await cache warming while request failures remain observable through `tap()`.\n- **`invalidateCache()` / `clearCache()`**: invalidate structured key prefixes without query state.\n- **`get()` / `post()` / `put()` / `patch()` / `delete()`**: conveniences for standard methods.\n- **`withBearerAuth()` / `withRequestId()` / `withLogging()`**: composable middleware configured at construction.\n- **`tap()`**: signal-owned structured transport observation.\n- **Error taxonomy**: HTTP, network, timeout, abort, parse, and schema failures are distinct, actionable classes.\n- **RFC 9457 problem details**: an `application/problem+json` error body parses onto `CourierHttpError.problem` while the HTTP status stays authoritative.\n- **`cancelAll()` / `dispose()`**: abort active requests and tear down the transport.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Flux](/flux/): composes async streams; pair with Courier transport calls in the owning state layer.\n- [Spell](/spell/): validates parsed HTTP payloads through Courier's `schema` option.\n- [Postmaster](/postmaster/): coordinates durable delivery of Courier requests through a job outbox.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: 'Courier: API Reference'\ndescription: Reference for Courier HTTP, cached read, prefetch, middleware, and error APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createCourier()` | Creates transport client | Sync | Dispose only when whole scope ends |\n| `request()` | Sends one HTTP request of any method | Async | Direct calls never deduplicate |\n| `get()` | Sends a GET, optionally through the explicit cache | Async | One key must identify one representation |\n| `prefetch()` | Warms a cached GET | Async | Request failures resolve after surfacing through `tap()` |\n| `invalidateCache()` / `clearCache()` | Removes cached values | Sync | Invalidation matches structured key prefixes |\n| `post()` / `put()` / `patch()` / `delete()` | Common write method conveniences | Async | Use `request()` for custom methods |\n| `withBearerAuth()` | Adds authorization middleware | Sync | Token provider runs per request |\n| `withRequestId()` | Adds request identifier middleware | Sync | Default generator uses `crypto.randomUUID()` |\n| `withLogging()` | Logs request result metadata | Sync | Requires explicit logger; URLs may contain sensitive query values |\n| `tap()` | Observe structured transport events | Sync | Observation only; optional signal owns lifetime |\n| `CourierHttpError.problem` | Parsed RFC 9457 problem details from an error body | Sync | Present only when the response is `application/problem+json` |\n| `ProblemDetail` | RFC 9457 problem members and extension members | Sync | Extension values are `unknown`; Courier does not validate their shapes |\n| `problemDetailError()` | Build an HTTP error that carries problem details | Sync | Intended for mocks and middleware, not for parsing a response |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/courier` | Client factory, errors, middleware helpers, and public types |\n\n## Client\n\n### `createCourier()`\n\n```ts\ncreateCourier(options?: CourierOptions): Courier;\n```\n\nReturns an HTTP client sharing base URL, default headers, immutable middleware, optional parsed-value cache, timeout, and cancellation lifecycle.\n\n| `CourierOptions` field | Type | Default | Description |\n| --- | --- | --- | --- |\n| `baseUrl` | `string` | `''` | Prefix for relative request paths |\n| `cache.capacity` | `number` | `100` | Maximum cached entries; positive integer or `Infinity` |\n| `cache.ttlMs` | `number` | `30_000` | Default cache freshness; non-negative milliseconds or `Infinity` |\n| `fetch` | `typeof globalThis.fetch` | `globalThis.fetch` | Fetch implementation |\n| `headers` | `HeadersInit` | `{}` | Default request headers (immutable; merged lowercase) |\n| `middleware` | `readonly Middleware[]` | `[]` | Immutable middleware chain configured at construction |\n| `timeout` | `number` | `30_000` | Integer milliseconds from 1 to 2,147,483,647, or `Infinity` |\n\n**Returns:** `Courier`.\n\n```ts\nimport { createCourier, withBearerAuth } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withBearerAuth('token')],\n});\n```\n\n| `Courier` member | Signature | Description |\n| --- | --- | --- |\n| `request` | `<T, P>(path, config?) => Promise<T>` | Sends one HTTP request; `config.method` defaults to `GET` |\n| `get` | `<T, P>(path, config?) => Promise<T>` | Sends GET; `cache` opts parsed responses into bounded caching |\n| `prefetch` | `<T, P>(path, config) => Promise<void>` | Warms one required structured cache key; failures are tap-observed |\n| `invalidateCache` | `(prefix: CourierCacheKey) => void` | Invalidates matching settled and pending key prefixes |\n| `clearCache` | `() => void` | Removes every cached and pending entry |\n| `post` | `<T, P>(path, config?) => Promise<T>` | Sends POST |\n| `put` | `<T, P>(path, config?) => Promise<T>` | Sends PUT |\n| `patch` | `<T, P>(path, config?) => Promise<T>` | Sends PATCH |\n| `delete` | `<T, P>(path, config?) => Promise<T>` | Sends DELETE |\n| `tap` | `(handler, options?: { signal?: AbortSignal }) => () => void` | Observes structured request and disposal events |\n| `cancelAll` | `() => void` | Aborts active requests; client remains usable |\n| `dispose` | `() => void` | Final teardown; aborts work and marks client unusable |\n| `disposed` | `boolean` | Whether final disposal occurred |\n| `disposalSignal` | `AbortSignal` | Aborts on final disposal |\n\n---\n\n## Requests\n\n### `request()`\n\n```ts\nrequest<T, P extends string>(path: P, config?: RequestConfig<P, T> & { method?: string }): Promise<T>;\n```\n\nSends one HTTP request. `config.method` defaults to `GET`; pass any method (including custom verbs) to support it consistently.\n\n| `RequestConfig<P, T>` field | Type | Description |\n| --- | --- | --- |\n| `method` | `string` | HTTP method; defaults to `GET` |\n| `params` | `Record<string, string \\| number \\| boolean>` | Path parameters for `{param}` placeholders |\n| `query` | `Params` | Query string parameters |\n| `body` | `unknown` | BodyInit values pass through; streams use Node duplex; GET/HEAD reject bodies; other values encode as JSON |\n| `headers` | `HeadersInit` | Per-request headers merged with (and overriding) defaults |\n| `responseType` | `ResponseType` | Response parsing strategy |\n| `schema` | `{ parse(data: unknown): T }` | Optional parsed-response validator; incompatible with `responseType: 'raw'` |\n| `signal` | `AbortSignal` | External abort signal merged with internal cancellation |\n| `timeout` | `number` | Integer milliseconds from 1 to 2,147,483,647, or `Infinity`; overrides default |\n| `fetchInit` | `Omit<RequestInit, 'body' \\| 'headers' \\| 'method' \\| 'signal'>` | Raw fetch options for advanced use |\n\n**Returns:** Parsed response body (or `undefined` for empty bodies). With\n`responseType: 'raw'` the resolved value is the `Response` itself; a `204`/`205`\nanswer whose transport carried an unexpected empty body stream is normalized to\n`body: null` — the stray stream is cancelled and headers, status, statusText,\nURL, redirect and type metadata are preserved (on the response and its clones).\nA null-body raw response owns no pending resource, so no consumption is required\nto release the request.\n\n```ts\nconst created = await courier.request<User>('/users', {\n method: 'POST',\n body: { name: 'Ada' },\n});\n```\n\n### `get()`\n\n```ts\nget<T, P extends string>(path: P, config?: GetRequestConfig<P, T>): Promise<T>;\n```\n\nConvenience for `request(path, { ...config, method: 'GET' })`. A `cache` descriptor opts a parsed response into the bounded cache. It requires a structured key and may override the default TTL. Raw responses cannot be cached.\n\n```ts\nconst user = await courier.get<User>('/users/{id}', {\n cache: { key: ['users', 1], ttlMs: 10_000 },\n params: { id: 1 },\n});\n```\n\n## Cache\n\n### `prefetch()`\n\n```ts\nprefetch<T, P extends string>(path: P, config: PrefetchConfig<P, T>): Promise<void>;\n```\n\nStarts the same cached GET used by `get()`. `config.cache` is required; `signal`, per-call `timeout`, and `responseType: 'raw'` are unavailable. Fresh values and pending loads are reused. The promise settles when the attempt finishes. Request failures emit `request-error` through `tap()` and resolve without caching data.\n\n### `invalidateCache()` and `clearCache()`\n\n```ts\ninvalidateCache(prefix: CourierCacheKey): void;\nclearCache(): void;\n```\n\n`invalidateCache()` removes every settled entry whose structured key starts with `prefix` and prevents matching pending loads from being stored. `clearCache()` applies that behavior to all entries. Existing waiters may still receive an invalidated in-flight result; `cancelAll()` or `dispose()` aborts physical work.\n\n---\n\n## Middleware\n\nMiddleware is an immutable chain configured at construction. Each middleware receives an immutable `FetchContext` and a `next` function.\n\n```ts\ntype Middleware = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;\n```\n\n### Middleware helpers\n\n```ts\nwithBearerAuth(token: string | (() => string | null | undefined | Promise<string | null | undefined>)): Middleware;\nwithRequestId(options?: { generate?: () => string; header?: string }): Middleware;\nwithLogging(options: {\n logger: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;\n}): Middleware;\n```\n\nEach helper returns a `Middleware` passed in the `middleware` option. `withLogging` requires an explicit `logger` function, isolates logger failures from requests, and has no default console output.\n\n```ts\nconst courier = createCourier({\n middleware: [\n withBearerAuth(() => tokenStore.getAccessToken()),\n withRequestId(),\n withLogging({ logger: (msg) => console.log(msg) }),\n ],\n});\n```\n\n## Types\n\n```ts\ntype TransportOptions = {\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n headers?: HeadersInit;\n middleware?: readonly Middleware[];\n timeout?: number;\n};\n\ntype CourierCacheKeyAtom = string | number | boolean | null;\ntype CourierCacheKey = readonly [CourierCacheKeyAtom, ...CourierCacheKeyAtom[]];\n\ntype CourierCacheOptions = {\n capacity?: number;\n ttlMs?: number;\n};\n\ntype CourierReadCache = {\n key: CourierCacheKey;\n ttlMs?: number;\n};\n\ntype CourierOptions = TransportOptions & {\n cache?: CourierCacheOptions;\n};\n\ntype FetchContext = {\n readonly headers: Readonly<Record<string, string>>;\n readonly init: Readonly<Omit<RequestInit, 'headers'>>;\n readonly url: string;\n withHeaders(updates: Record<string, string | undefined>): FetchContext;\n withInit(updates: Partial<Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>>): FetchContext;\n};\n\ntype Middleware = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;\n\ntype CourierEvent =\n | { type: 'request-start'; method: string; url: string }\n | { type: 'request-success'; method: string; url: string; status: number; duration: number }\n | { type: 'request-error'; method: string; url: string; error: unknown }\n | { type: 'dispose' };\n```\n\n```ts\ntype ParamValue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;\ntype Params = Record<string, ParamValue>;\ntype ResponseType = 'auto' | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'raw';\ntype RequestConfig<P extends string = string, T = unknown> = {\n body?: unknown;\n method?: string;\n fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: HeadersInit;\n params?: Record<string, string | number | boolean>;\n query?: Params;\n responseType?: ResponseType;\n schema?: { parse(data: unknown): T };\n signal?: AbortSignal;\n timeout?: number;\n};\n\ntype GetRequestConfig<P extends string = string, T = unknown> = Omit<\n RequestConfig<P, T>,\n 'body' | 'method' | 'responseType'\n> &\n ({ cache?: CourierReadCache; responseType?: Exclude<ResponseType, 'raw'> } | { cache?: never; responseType: 'raw' }) & {\n body?: never;\n };\n\ntype PrefetchConfig<P extends string = string, T = unknown> = Omit<\n GetRequestConfig<P, T>,\n 'cache' | 'responseType' | 'signal' | 'timeout'\n> & {\n cache: CourierReadCache;\n responseType?: Exclude<ResponseType, 'raw'>;\n signal?: never;\n timeout?: never;\n};\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `CourierError` | Base class for all Courier errors | Use `instanceof` to narrow |\n| `CourierHttpError` | Non-2xx HTTP response | `status`, `data`, `headers`, `method`, `url`, `problem`; `CourierHttpError.is(e, status?)` narrows by status |\n| `CourierNetworkError` | Request failure without response | `method`, `url`, `cause` |\n| `CourierTimeoutError` | Timeout signal aborts request | `method`, `url`, `cause` |\n| `CourierAbortError` | Caller or client cancellation | `method`, `url`, `cause` |\n| `CourierSchemaValidationError` | Response schema fails | `data`, `cause` |\n| `CourierParseError` | Response body cannot parse | N/A |\n| `CourierConfigError` | Invalid request config (GET/HEAD body, raw + schema) | N/A |\n| `CourierDisposedError` | Work starts after disposal | N/A |\n\nErrors thrown directly by custom middleware are preserved rather than reclassified.\n\n### Problem details\n\n```ts\nexport type ProblemDetail = Readonly<{\n type?: string;\n status?: number;\n title?: string;\n detail?: string;\n instance?: string;\n [extension: string]: unknown;\n}>;\n\nexport const PROBLEM_JSON_MEDIA_TYPE = 'application/problem+json';\n\nexport function isProblemMediaType(contentType: string | null | undefined): boolean;\n\nexport function problemDetailError(\n problem: ProblemDetail,\n options?: { message?: string; method?: string; status?: number; url?: string },\n): CourierHttpError;\n```\n\nWhen a non-2xx response answers with `application/problem+json` (RFC 9457), `CourierHttpError.problem`\nholds a shallow-frozen copy of the parsed JSON object and the error message prefers `detail`, then\n`title`, then the response status text (or `HTTP <status>` when status text is empty). Any other error\nbody keeps the shape it already had on `data`, with `problem` left `undefined`. Courier checks that\nthe parsed body is an object; it does not validate the types or contents of registered or extension\nmembers. RFC 9457 treats an omitted `type` as `about:blank`, but Courier does not insert that value\ninto the object.\n\n| Member | Purpose | Common gotcha |\n| --- | --- | --- |\n| `ProblemDetail` | The five registered RFC 9457 members plus extensions | Every member is optional; extension values have type `unknown` |\n| `CourierHttpError.problem` | Parsed problem document | For response errors, it is a frozen shallow copy and is `undefined` unless the media type matches and the body is an object |\n| `PROBLEM_JSON_MEDIA_TYPE` | Registered media type constant | Compare with `isProblemMediaType()`, not `===`, to allow parameters |\n| `isProblemMediaType()` | Match `application/problem+json` and its parameterised forms | Plain `application/json` never matches |\n| `problemDetailError()` | Build a problem-carrying `CourierHttpError` without a server | Status defaults to numeric `problem.status`, then `500`; a supplied `options.status` takes precedence |\n\n```ts\nproblemDetailError(problem, options?)\n```\n\n`problemDetailError()` returns a `CourierHttpError` with the supplied problem object on both\n`error.problem` and `error.data`. Its defaults are `method: 'GET'`, `url: ''`, and status from\n`problem.status` when numeric or `500` otherwise. The error message uses `options.message`, then\n`problem.detail`, `problem.title`, and finally `HTTP <status>`. Unlike a parsed response problem,\nthe supplied object is not copied or frozen.\n\nThe HTTP status line stays authoritative: `error.status` is the response status even when\n`problem.status` disagrees. A JSON array or primitive leaves `data` as that parsed value; malformed\nJSON leaves `data` as its raw text. Neither produces a partial `problem`.\n\n```ts\nimport { createCourier, CourierHttpError, problemDetailError, type ProblemDetail } from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: '/api' });\n\ntry {\n await courier.post('/invoices');\n} catch (error) {\n if (CourierHttpError.is(error)) {\n const problem: ProblemDetail | undefined = error.problem;\n console.log(error.status, problem?.title, problem?.detail, problem?.type);\n }\n}\n\nconst mockedError = problemDetailError(\n { title: 'Insufficient credit', detail: 'Choose a lower amount.', status: 402 },\n { method: 'POST', url: '/invoices' },\n);\nconsole.log(mockedError.status, mockedError.problem?.detail);\n```\n",
|
|
6
|
+
"usage": "---\ntitle: 'Courier: Usage Guide'\ndescription: Use one Courier client for typed HTTP, explicit cached reads, prefetching, immutable middleware, and structured errors.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one Courier client for an application or request scope. Its transport policy, middleware, and disposal lifecycle apply to every request.\n\n```ts\nimport { createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\nconst user = await courier.get<User>('/users/{id}', { params: { id: 1 } });\nconsole.log(user.name);\n```\n\n## HTTP Requests\n\nUse `get()`, `post()`, `put()`, `patch()`, and `delete()` for standard methods. Use `request()` when the method is custom or selected dynamically. Courier preserves root-relative paths, encodes path parameters, serializes JSON-compatible bodies, and parses successful responses.\n\n```ts\nconst posts = await courier.get<{ id: number; title: string }[]>('/users/{id}/posts', {\n params: { id: 1 },\n query: { limit: 20, status: 'published' },\n});\n\nawait courier.patch('/posts/{id}', {\n body: { title: 'Updated title' },\n params: { id: posts[0].id },\n});\n```\n\n`get()`, `post()`, `put()`, `patch()`, and `delete()` cover common methods. Use `request()` for custom verbs or when a method is selected dynamically.\n\n## Default Headers\n\nSet default headers at construction. They are merged (lowercased) into every request and cannot be mutated at runtime: middleware handles dynamic headers.\n\n```ts\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n headers: { 'x-client': 'web' },\n});\n```\n\n## Middleware\n\nMiddleware is an immutable chain configured once at construction. Each middleware receives an immutable `FetchContext` and a `next` function. Use `ctx.withHeaders()` or `ctx.withInit()` to derive a new context: never mutate `ctx` directly.\n\n```ts\nimport { withBearerAuth, withRequestId, createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [\n withBearerAuth(() => sessionStorage.getItem('access-token') ?? ''),\n withRequestId(),\n ],\n});\n```\n\nA bearer token provider may return `null`, `undefined`, or an empty string to omit a stale authorization header. Use `withLogging()` with an explicit `logger` function to log requests during local development. `withLogging()` includes full URLs, so sanitize query values before persistent logging.\n\n```ts\nimport { withLogging, createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withLogging({ logger: (msg) => console.log(msg) })],\n});\n```\n\nUse `tap()` for multiple structured transport observers. It emits request-start, request-success, request-error, and dispose events; observer failures cannot alter requests. Pass `{ signal }` to own listener lifetime.\n\n```ts\nconst stop = courier.tap((event) => metrics.record(event), { signal: routeSignal });\n```\n\nBecause middleware is immutable, a client's behavior is locally understandable from its construction call. To change policy, create a new client.\n\n## Cached Reads and Prefetching\n\nOpt parsed GETs into caching with a structured key. Courier uses a finite capacity of 100 entries and a 30-second TTL by default; client options change those defaults, and each read may override TTL. Successful parsed and schema-validated values are cached, while errors are not.\n\n```ts\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n cache: { capacity: 200, ttlMs: 60_000 },\n});\nconst cache = { key: ['accounts', accountId, 'users', userId] as const, ttlMs: 10_000 };\n\nawait courier.prefetch<User>('/users/{id}', { cache, params: { id: userId } });\nconst user = await courier.get<User>('/users/{id}', { cache, params: { id: userId } });\n```\n\n`prefetch()` returns a promise that settles when warming finishes. Request failures are not cached or rejected from that promise; the physical request emits `request-error` through `tap()`. Cache, key, and URL configuration errors still throw before prefetch starts.\n\nInvalidate a structured prefix after writes. Every matching settled or pending key becomes ineligible for caching, while unrelated entries remain available.\n\n```ts\nawait courier.patch('/users/{id}', { body: update, params: { id: userId } });\ncourier.invalidateCache(['accounts', accountId, 'users']);\n```\n\nA cache key must identify one response representation, including a stable non-secret principal or tenant identifier, schema, and relevant request variants. Never place credentials in a key; call `clearCache()` when identity changes. Cached objects are shared by reference.\n\nConcurrent callers share physical work but own their own signal and timeout. One cancelled caller does not affect the others. When every caller cancels and no prefetch owns the load, Courier aborts the physical request. `cancelAll()` always aborts physical work; `dispose()` also clears settled cache data.\n\n## Response Parsing\n\nCourier parses successful 2xx response bodies automatically based on `content-type`, unless you set `responseType` explicitly (`'json'`, `'text'`, `'blob'`, `'arrayBuffer'`, or `'raw'` for the `Response` itself). Empty JSON bodies return `undefined` instead of throwing. Raw response bodies remain owned by Courier until consumed or cancelled, so `cancelAll()` and `dispose()` still abort them. Raw responses cannot be combined with `schema`. Always consume or cancel the body to release request ownership, and set `timeout: Infinity` for intentionally long-lived streams. Courier preserves URL, redirect, and type metadata on tracked responses and their clones. A `204`/`205` raw answer is normalized to `body: null` — a stray empty transport stream is cancelled for you — so a null-body raw response never carries pending body ownership and needs no consumption.\n\n```ts\nconst buffer = await courier.request('/files/{id}', {\n method: 'GET',\n params: { id: 1 },\n responseType: 'arrayBuffer',\n});\n```\n\n## Schema Validation\n\nPass a `schema` with a `parse(data)` method to validate the parsed body. Any validator works: a `@vielzeug/spell` schema or a plain function wrapper. A failed parse throws `CourierSchemaValidationError`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst user = await courier.get('/users/1', {\n schema: s.object({ id: s.number(), name: s.string() }),\n});\n```\n\n## Cancellation and Disposal\n\nPass a `signal` to cancel one request. `cancelAll()` aborts every active request without disposing the client. `dispose()` aborts active work and marks the client unusable: call it only at the final application or request boundary.\n\n```ts\nconst controller = new AbortController();\nvoid courier.get('/slow', { signal: controller.signal });\ncontroller.abort(); // cancels that request only\n\ncourier.cancelAll(); // cancels all active requests\ncourier.dispose(); // final teardown\n```\n\n## Timeouts\n\nSet a client-wide `timeout` (default 30,000 ms) or a per-request timeout. Finite values must be integer milliseconds from 1 through 2,147,483,647. A timed-out request rejects with `CourierTimeoutError`; use `Infinity` for intentionally long-lived work.\n\n```ts\nconst courier = createCourier({ timeout: 10_000 });\nawait courier.get('/health', { timeout: 2_000 });\n```\n\n## Error Handling\n\nCourier classifies URL, request-construction, fetch, cancellation, HTTP, response-parsing, and schema failures into distinct error classes. Errors thrown directly by custom middleware are preserved.\n\n```ts\nimport {\n CourierAbortError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierTimeoutError,\n createCourier,\n} from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\n\nasync function loadUser(): Promise<string> {\n try {\n return (await courier.get<{ name: string }>('/users/1', { timeout: 2_000 })).name;\n } catch (error) {\n if (error instanceof CourierAbortError) return 'Cancelled';\n if (error instanceof CourierTimeoutError) return 'Timed out; retry.';\n if (CourierHttpError.is(error, 404)) return 'User not found.';\n if (error instanceof CourierNetworkError) return 'Check your connection.';\n if (error instanceof CourierParseError) return 'The server returned an unreadable response.';\n throw error;\n }\n}\n```\n\n- A `CourierHttpError` has a response (`status`, `data`, `headers`); a `CourierNetworkError` does not.\n- Do not display cancellation as an application error during navigation.\n- `CourierSchemaValidationError` carries the raw pre-validation `data`.\n\n## Problem Details\n\nWhen a server answers a failure with `application/problem+json` (RFC 9457), Courier parses the\nJSON object onto `CourierHttpError.problem` and chooses the message from `detail`, then `title`,\nthen the HTTP status text. The parsed object is shallow-frozen; extension members remain `unknown`\nand Courier does not validate their contents. Nothing is configured.\n\n```ts\nimport { CourierHttpError, createCourier, type ProblemDetail } from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: '/api' });\n\nasync function submitInvoice(body: unknown): Promise<string> {\n try {\n await courier.post('/invoices', { body });\n return 'Saved.';\n } catch (error) {\n if (!CourierHttpError.is(error)) throw error;\n\n const problem = error.problem;\n if (problem?.title === 'Insufficient credit') return `Blocked: ${problem.detail ?? 'no credit'}`;\n\n // Extension members are ordinary properties on the same document.\n const field = problem?.['errors'] as Record<string, string> | undefined;\n\n return field ? Object.values(field).join(', ') : `Request failed (${error.status}).`;\n }\n}\n```\n\n`error.status` is always the HTTP status from the response, so a `problem.status` that disagrees\nnever overrides it. `problem` stays `undefined` for any other content type, and a `problem+json`\nbody that is a JSON array or primitive keeps that parsed value on `data`; malformed JSON keeps its\nraw text there. Neither case creates a partial `problem`. RFC 9457 treats an omitted `type` as\n`about:blank`, but Courier does not add a `type` property. Use `problemDetailError()` to raise the\nsame error shape from a mock or middleware; it stores the supplied problem object directly on both\n`problem` and `data`.\n\n```ts\nimport { problemDetailError } from '@vielzeug/courier';\n\nconst mockFailure = problemDetailError(\n { title: 'Insufficient credit', detail: 'Choose a lower amount.', status: 402 },\n { method: 'POST', url: '/invoices' },\n);\n\nconsole.log(mockFailure.status, mockFailure.problem?.detail);\n```\n\n## Composing with a State Layer\n\nCourier owns HTTP transport and optional parsed GET caching. Observable query state, optimistic mutations, collection navigation, and stream state belong to the owning state layer (e.g. Sourcerer, TanStack Query, or consumer code). Compose Courier calls into that layer instead of nesting state machines.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst users = createPageSource<User>({\n load: async ({ page, pageSize, signal }) => {\n const result = await courier.get<{ data: User[]; total: number }>('/users', {\n query: { page, pageSize },\n signal,\n });\n return { items: result.data, totalItems: result.total };\n },\n});\n\nawait users.reload();\n```\n\n## Framework Integration\n\nCreate Courier at an application or route boundary. Inject the client where requests are issued; let the framework own rendering and cache state.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useContext } from 'react';\nimport { createCourier } from '@vielzeug/courier';\n\ntype User = { id: number; name: string };\n\ntype CourierClient = ReturnType<typeof createCourier>;\nconst CourierContext = createContext<CourierClient | null>(null);\n\nexport function CourierProvider({ children, courier }: { children: React.ReactNode; courier: CourierClient }) {\n return <CourierContext.Provider value={courier}>{children}</CourierContext.Provider>;\n}\n\nexport function useCourier() {\n const courier = useContext(CourierContext);\n if (!courier) throw new Error('CourierProvider missing');\n return courier;\n}\n\n// In a component:\n// const user = await useCourier().get<User>('/users/1');\n```\n\n```ts [Vue 3]\nimport { createCourier } from '@vielzeug/courier';\n\nconst courier = createCourier({ baseUrl: '/api' });\n// Provide via app.provide('courier', courier) and inject in setup().\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createCourier } from '@vielzeug/courier';\n export let courier: ReturnType<typeof createCourier>;\n</script>\n```\n\n:::\n\nCourier exposes no loading, error, or cache store. Construct the client at the application or SSR-request boundary, pass it into framework providers, and dispose it at that same boundary. Render state in the framework that owns the view.\n\n## Gotchas\n\n### Root-relative paths stay root-relative\n\nWith no `baseUrl`, `'/api/users'` is passed to fetch unchanged while `'api/users'` remains document-relative. With a `baseUrl`, Courier appends either spelling to that base.\n\n### `baseUrl` should not include query parameters\n\n`buildUrl` joins `baseUrl` and path with `/`. A base URL like `https://api.example.com?token=abc` produces broken URLs (`https://api.example.com?token=abc/users`). Pass query parameters per-request via `config.query` instead.\n\n### Query parameters precede fragments\n\nWhen a path already contains `#fragment`, Courier inserts `config.query` before the fragment so values remain part of the HTTP query.\n\n### GET and HEAD bodies are rejected\n\nFetch does not support bodies for GET or HEAD. Courier rejects them as request parse errors before dispatch.\n\n### Middleware is immutable\n\nThere is no `use()` method. To change middleware, create a new client. This keeps each client's behavior locally understandable and avoids runtime mutation of the request chain.\n\n### Keep credentials out of URLs when using logging middleware\n\n`withLogging()` logs the full URL, including query parameters. If URLs may contain sensitive data, provide a custom `logger` that sanitizes the URL first.\n\n## Best Practices\n\n- Create one Courier client per application or SSR request scope.\n- Configure middleware once at construction; never mutate it at runtime.\n- Use method conveniences for GET/POST/PUT/PATCH/DELETE and `request()` for custom verbs.\n- Use explicit cache keys only for parsed GETs that should share data and in-flight work.\n- Clear cached data when the authenticated principal or tenant changes.\n- Let the owning state layer own observable queries and mutations.\n- Dispose only at the final application or request boundary.\n- Keep credentials out of URLs when using logging middleware.\n",
|
|
7
7
|
"examples": "---\ntitle: 'Courier: Examples'\ndescription: Practical examples and recipes for courier.\n---\n\n## Examples\n\n- [Authentication](./examples/authentication.md)\n- [Service Discovery and Custom Fetch](./examples/service-discovery-and-custom-fetch.md)\n- [CRUD Operations](./examples/crud-operations.md)\n- [Disposal](./examples/disposal.md)\n- [Error Handling Patterns](./examples/error-handling-patterns.md)\n- [File Uploads](./examples/file-uploads.md)\n- [Optimistic Updates](./examples/optimistic-updates.md)\n- [SSE Events](./examples/sse-events.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
@@ -21,25 +21,34 @@
|
|
|
21
21
|
"id": "middleware",
|
|
22
22
|
"code": "import { createCourier, withBearerAuth, withRequestId } from '@vielzeug/courier'\n\nconst fetch: typeof globalThis.fetch = async (_url, init) =>\n new Response(JSON.stringify({ ok: true }), { headers: { 'content-type': 'application/json' } })\n\nlet token = 'initial-token'\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n fetch,\n middleware: [withBearerAuth(() => token), withRequestId({ generate: () => 'req-1' })],\n})\n\nawait courier.get('/profile')\nconsole.log('First request sent with initial token')\n\ntoken = 'refreshed-token'\nawait courier.request('/profile', { method: 'POST', body: { update: true } })\nconsole.log('Second request sent with refreshed token')\n\ncourier.dispose()\nconsole.log('✓ Client disposed')",
|
|
23
23
|
"name": "middleware - Immutable Chain"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "problem-details",
|
|
27
|
+
"code": "import { CourierHttpError, createCourier } from '@vielzeug/courier'\n\nconst fetch: typeof globalThis.fetch = async () =>\n new Response(\n JSON.stringify({\n type: 'https://api.example.com/problems/insufficient-credit',\n status: 402,\n title: 'Insufficient credit',\n detail: 'Account 1 owes 12.50 EUR.',\n errors: { amount: 'above the remaining balance' },\n }),\n { status: 402, headers: { 'content-type': 'application/problem+json; charset=utf-8' } },\n )\n\nconst courier = createCourier({ baseUrl: 'https://api.example.com', fetch })\n\ntry {\n await courier.post('/invoices', { body: { amount: 12.5 } })\n} catch (error) {\n if (CourierHttpError.is(error)) {\n console.log('Status:', error.status)\n console.log('Message:', error.message)\n console.log('Title:', error.problem?.title)\n console.log('Extension:', error.problem?.errors)\n }\n}\n\ncourier.dispose()\nconsole.log('✓ Problem details parsed from the error body')",
|
|
28
|
+
"name": "problem+json - RFC 9457 error bodies"
|
|
24
29
|
}
|
|
25
30
|
],
|
|
26
31
|
"typeSignatures": {
|
|
27
|
-
"Courier": "export {
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
32
|
+
"Courier": "export { type Courier, type CourierEvent, type CourierOptions, createCourier } from './courier.js';",
|
|
33
|
+
"CourierEvent": "export { type Courier, type CourierEvent, type CourierOptions, createCourier } from './courier.js';",
|
|
34
|
+
"CourierOptions": "export { type Courier, type CourierEvent, type CourierOptions, createCourier } from './courier.js';",
|
|
35
|
+
"createCourier": "export { type Courier, type CourierEvent, type CourierOptions, createCourier } from './courier.js';",
|
|
36
|
+
"CourierAbortError": "export {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';",
|
|
37
|
+
"CourierConfigError": "export {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';",
|
|
38
|
+
"CourierDisposedError": "export {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';",
|
|
39
|
+
"CourierError": "export {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';",
|
|
40
|
+
"CourierHttpError": "export {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';",
|
|
41
|
+
"CourierNetworkError": "export {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';",
|
|
42
|
+
"CourierParseError": "export {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';",
|
|
43
|
+
"CourierSchemaValidationError": "export {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';",
|
|
44
|
+
"CourierTimeoutError": "export {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';",
|
|
45
|
+
"problemDetailError": "export {\n CourierAbortError,\n CourierConfigError,\n CourierDisposedError,\n CourierError,\n CourierHttpError,\n CourierNetworkError,\n CourierParseError,\n CourierSchemaValidationError,\n CourierTimeoutError,\n problemDetailError,\n} from './errors.js';",
|
|
40
46
|
"withBearerAuth": "export { withBearerAuth, withLogging, withRequestId } from './interceptors.js';",
|
|
41
47
|
"withLogging": "export { withBearerAuth, withLogging, withRequestId } from './interceptors.js';",
|
|
42
48
|
"withRequestId": "export { withBearerAuth, withLogging, withRequestId } from './interceptors.js';",
|
|
49
|
+
"isProblemMediaType": "export { isProblemMediaType, PROBLEM_JSON_MEDIA_TYPE, type ProblemDetail } from './problem.js';",
|
|
50
|
+
"PROBLEM_JSON_MEDIA_TYPE": "export { isProblemMediaType, PROBLEM_JSON_MEDIA_TYPE, type ProblemDetail } from './problem.js';",
|
|
51
|
+
"ProblemDetail": "export { isProblemMediaType, PROBLEM_JSON_MEDIA_TYPE, type ProblemDetail } from './problem.js';",
|
|
43
52
|
"ResponseType": "export type { ResponseType } from './response.js';",
|
|
44
53
|
"FetchContext": "export type { FetchContext, Middleware, TransportOptions } from './transport.js';",
|
|
45
54
|
"Middleware": "export type { FetchContext, Middleware, TransportOptions } from './transport.js';",
|
package/data/packages/ore.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "export type { ComponentDefinition } from './component-types';\nexport { createContext, type InjectionKey, inject, injectStrict, provide } from './context';\nexport { define } from './define';\n// Near-universal template directives: used in most non-trivial components (lists,\n// conditionals, and class/style maps. Kept in the main entry alongside\n// `html`/`define` rather than a separate sub-path: tree-shaking already means an unused export\n// costs nothing in a bundled consumer, so splitting these off only adds an extra import line\n// for functionality most components need on day one. `unsafeHtml()` and `live()` remain here\n// too: their explicit names make their specialized behavior clear without a second import path.\nexport { classMap } from './directives/classMap';\nexport { each } from './directives/each';\nexport { type LiveBinding, live } from './directives/live';\nexport { styleMap } from './directives/styleMap';\nexport { unsafeHtml } from './directives/unsafe-html';\nexport { when } from './directives/when';\nexport { OreApiError, OreError, type OreErrorPhase, OreInternalError, OreLifecycleError } from './errors';\nexport { type FormFieldHandle, type FormFieldOptions, useField } from './forms/field';\nexport {\n type AttributeBindings,\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindingValue,\n} from './host-bind';\nexport type { InferProps, PropDef, PropInputDefs, PropsDef } from './props';\nexport { prop } from './props';\n// Lifecycle hooks: plain functions, called during setup() or a composable it invokes.\nexport {\n getHost,\n type OnFormResetCallback,\n type OnMountedCallback,\n onCleanup,\n onElement,\n onEvent,\n onFormReset,\n onMounted,\n watchEffect,\n} from './runtime';\nexport { type ComponentSlots, useSlots } from './slots';\nexport { html } from './template/instantiator';\nexport { type HTMLResult, type Ref, type RefCallback, ref } from './template/result';\nexport { type CSSResult, css } from './utils/css';\nexport type { MaybeReactive } from './utils/dom';\nexport { type EmitFn, useEmit } from './utils/emit';\n\nexport { createId } from './utils/id';\n",
|
|
2
|
+
"apiSource": "export type { ComponentDefinition } from './component-types';\nexport { createContext, type InjectionKey, inject, injectStrict, provide } from './context';\nexport { define } from './define';\n// Near-universal template directives: used in most non-trivial components (lists,\n// conditionals, and class/style maps. Kept in the main entry alongside\n// `html`/`define` rather than a separate sub-path: tree-shaking already means an unused export\n// costs nothing in a bundled consumer, so splitting these off only adds an extra import line\n// for functionality most components need on day one. `unsafeHtml()` and `live()` remain here\n// too: their explicit names make their specialized behavior clear without a second import path.\nexport { classMap } from './directives/classMap';\nexport { each } from './directives/each';\nexport { type LiveBinding, live } from './directives/live';\nexport { type ModelOptions, model } from './directives/model';\nexport { styleMap } from './directives/styleMap';\nexport { unsafeHtml } from './directives/unsafe-html';\nexport { when } from './directives/when';\nexport { OreApiError, OreError, type OreErrorPhase, OreInternalError, OreLifecycleError } from './errors';\nexport { type FormFieldHandle, type FormFieldOptions, type FormFieldValidity, useField } from './forms/field';\nexport {\n type AttributeBindings,\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindingValue,\n} from './host-bind';\nexport type { InferProps, PropDef, PropInputDefs, PropsDef } from './props';\nexport { prop } from './props';\n// Lifecycle hooks: plain functions, called during setup() or a composable it invokes.\nexport {\n getHost,\n type OnFormResetCallback,\n type OnMountedCallback,\n onCleanup,\n onElement,\n onEvent,\n onFormReset,\n onMounted,\n watchEffect,\n} from './runtime';\nexport { type ComponentSlots, useSlots } from './slots';\nexport { html } from './template/instantiator';\nexport { type HTMLResult, type Ref, type RefCallback, ref } from './template/result';\nexport { type CSSResult, css } from './utils/css';\nexport type { MaybeReactive } from './utils/dom';\nexport { type EmitFn, useEmit } from './utils/emit';\n\nexport { createId } from './utils/id';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: 'Ore: Web component authoring with signals'\ndescription: Functional custom-element authoring with typed props, reactive templates, lifecycle helpers, and testing utilities.\npackage: ore\ncategory: ui-primitives\nkeywords: [web-components, custom-elements, reactive, templates, signals, lifecycle]\nrelated: [ripple, refine, orbit]\nexports: [define, prop, html, css, ref, createContext, inject, injectStrict, provide, onMounted, onCleanup, onEvent, onElement, onFormReset, watchEffect, useEmit, useSlots, getHost, bind, each, when, classMap, styleMap, live, unsafeHtml, useField, createId, OreError, OreApiError, OreInternalError, OreLifecycleError, AttributeBindings, BindOptions, MaybeReactive]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ore\" />\n\n## Why Ore?\n\nOre keeps custom elements functional and signal-driven while giving you direct control over templates, lifecycle hooks, host bindings, and form-associated behavior.\n\n```ts\n// Before: vanilla custom element boilerplate\nclass MyCounter extends HTMLElement {\n #count = 0;\n connectedCallback() {\n this.attachShadow({ mode: 'open' });\n this.#render();\n }\n #render() {\n this.shadowRoot!.innerHTML = `<button>${this.#count}</button>`;\n this.shadowRoot!.querySelector('button')!.onclick = () => {\n this.#count++;\n this.#render();\n };\n }\n}\ncustomElements.define('my-counter', MyCounter);\n\n// After: Ore\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('my-counter', {\n setup() {\n const count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n },\n});\n```\n\n| Feature | Ore | Lit | Stencil |\n| -------------------------- | ------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"ore\" type=\"size\" /> | ~12 kB | ~60 kB+ toolchain |\n| Signal-first runtime | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> (separate signals package) | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Functional component setup | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed prop helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Host binding helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | Partial |\n| Form-associated helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Partial |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Ore when** you want typed, signal-driven custom elements with minimal runtime overhead and no framework lock-in.\n\n**Consider Lit when** you need a mature ecosystem with wide community adoption and don't need signal-based reactivity.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ore @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ore @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ore @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { bind, css, define, html, onMounted, prop } from '@vielzeug/ore';\n\ndefine('my-counter', {\n props: {\n label: prop.string('Count'),\n step: prop.number(1),\n },\n styles: [\n css`\n :host {\n display: inline-grid;\n gap: 0.5rem;\n }\n `,\n ],\n setup(props) {\n const count = signal(0);\n const doubled = computed(() => count.value * 2);\n\n bind({ class: { 'is-positive': () => count.value > 0 } });\n\n onMounted(() => console.log('mounted'));\n\n return html`\n <button @click=${() => (count.value += props.step.value)}>${props.label}: ${count}</button>\n <p>Doubled: ${doubled}</p>\n `;\n },\n});\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Signal-first runtime with `signal`, `computed`, `watch`, `batch` from `@vielzeug/ripple`: import them directly\n- Functional component authoring via `define(tag, { props, setup, styles, formAssociated })`\n- Props via `prop.*` helpers (`prop.string`, `prop.number`, `prop.bool`, `prop.oneOf`, `prop.json`, `prop.data`) or raw `PropDef` objects\n- `setup(props)` takes only props and returns an `HTMLResult` directly: `return html\\`...\\``\n- Lifecycle hooks: `onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`: plain functions imported from `@vielzeug/ore`, called directly from `setup()` or any composable it calls\n- Directives: `each` (keyed
|
|
5
|
-
"api": "---\ntitle: 'Ore: API Reference'\ndescription: Complete API reference for @vielzeug/ore and @vielzeug/ore/testing.\n---\n\n[[toc]]\n\n## API Overview\n\nAll browser-runtime symbols below are imported from `@vielzeug/ore`. Lifecycle/context/binding functions (`onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`, `bind`, `provide`, `useEmit`, `useSlots`, `getHost`) resolve the active component through an implicit \"current component\" context: they work when called synchronously during `setup()`, or from any composable function `setup()` calls (transitively), but throw if called outside that window.\n\n> `watchEffect` is not named `watch`: `@vielzeug/ripple` already exports a `watch(source, callback)` with different semantics (explicit source + old/new value pair), and the two are frequently imported in the same file.\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ---------------------- | ----------------------------------------------------- | -------------- | -------------------------------------------------------------------------- |\n| `define()` | Create and register a custom element | Sync | Tag must contain a hyphen; call before first use |\n| `html` | Tagged template literal returning HTMLResult | Sync | Expressions must be signals, functions, or primitives |\n| `prop.*` | Typed prop helpers (string, bool, number, …) | Sync | Prop values are signals: read `.value` |\n| `provide()`/`inject()` | Context API for parent-to-descendant sharing | Setup only | Must be called synchronously during `setup()` |\n| `ref()` | Reactive reference to a DOM element | Sync | Value is null until after first mount |\n| `createContext()` | Create a unique typed injection key | Sync | Context is scoped to the component tree |\n| `each()` | Keyed list rendering with DOM diffing | Sync | Duplicate keys report `ore:error`; plain `T[]` is a one-time static render |\n| `when()` | Conditional branch rendering | Sync | Getter-fn computed disposed on cleanup; static bool skips subscription |\n| `live(signal)` | One-way binding that skips stale writes during input | Sync | Use for controlled inputs alongside a manual `@input` handler |\n| `onMounted(fn)` | DOM-ready callback | Setup only | Must be called synchronously during `setup()` |\n| `onCleanup(fn)` | Register teardown | Setup only | Called on component disconnect |\n| `onEvent(target, …)` | Scoped event listener with auto-cleanup | Setup only | No-ops on null target; removed on disconnect |\n| `useField(options)` | Wire signal to form `ElementInternals` | Setup only | Requires `formAssociated: true` on the component definition |\n| `onFormReset(fn)` | Run work when the ancestor `<form>` resets | Setup only | Fires every reset (not one-shot); only for `formAssociated: true` components |\n| `useEmit<Emits>()` | Typed `emit()` bound to the current host | Setup only | Call once per component; returns `dispatchEvent`'s boolean (`false` if a listener called `preventDefault()`) |\n| `useSlots<SlotNames>()`| Reactive slot presence/element signals | Setup only | Safe to call more than once: the underlying registry is created once |\n| `getHost()` | The current component's host element | Setup only | Prefer a higher-level helper (`bind`, …) when one exists |\n\n## Package Entry Points\n\n| Import | Purpose |\n| ------------------------- | ------------------------------------------------------------------ |\n| `@vielzeug/ore` | All browser runtime APIs, including directives, fields, and lifecycle helpers |\n| `@vielzeug/ore/testing` | Ore-specific mounting, lifecycle, hook, cleanup, and form test support |\n| `@vielzeug/assay` | Generic DOM events, scoped queries, and async waiting |\n\n## Core Component API\n\n### `define(tag, definition)`\n\n```ts\ndefine<Props>(tag: string, definition: ComponentDefinition<Props>): CustomElementConstructor;\n```\n\nReturns the registered class (for `instanceof` or subclassing); registration itself is the only global side effect.\n\nThe `setup()` function receives only typed prop signals:\n\n```ts\nsetup(props) {\n return html`<div>${props.label}</div>`;\n}\n```\n\nEverything else: lifecycle hooks, host bindings, context, slots, emit: is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, html, onMounted, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-card', {\n setup(_props) {\n const emit = useEmit<{ close: undefined }>();\n const slots = useSlots<'header' | 'footer'>();\n\n onMounted(() => console.log('mounted'));\n\n // emit() returns dispatchEvent's boolean: false if a listener called preventDefault()\n const notCancelled = emit('close');\n\n return html`${when(slots.has('header'), () => html`<slot name=\"header\"></slot>`)}`;\n },\n});\n```\n\n`useEmit<Emits>()` and `useSlots<SlotNames>()` are factory hooks: call them once per setup run to get a typed\n`emit`/`slots` bound to the current host. `useSlots()` is safe to call more than once within that setup run.\n\n### ComponentDefinition\n\n```ts\ntype ComponentDefinition<Props> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM (no shadow root)\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n```\n\n## Runtime Helpers\n\n`onMounted`, `onCleanup`, `onEvent`, `onElement`, and `watchEffect` are plain functions imported from `@vielzeug/ore`. Call them directly during `setup()`.\n\n```ts\nimport { html, onCleanup, onEvent, onMounted } from '@vielzeug/ore';\n\nsetup(props) {\n onMounted(() => {\n // DOM is ready; return a function for mount-scoped cleanup\n return () => { /* cleanup on unmount */ };\n });\n\n onCleanup(() => { /* called on disconnect */ });\n\n onEvent(window, 'keydown', (e) => { /* auto-removed on disconnect */ });\n\n return html`...`;\n}\n```\n\nBecause these resolve the active component through an implicit context (rather than a value threaded through parameters), composable helper functions can call them directly too: no need to pass hooks in as options:\n\n```ts\nimport { onCleanup } from '@vielzeug/ore';\n\nfunction useMyHelper() {\n onCleanup(() => { /* teardown */ });\n}\n\n// In setup:\nsetup(_props) {\n useMyHelper();\n return html`...`;\n}\n```\n\n## Props API\n\n| Helper | Signature | Notes |\n| ----------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------ |\n| `prop.string(defaultValue?)` | `PropDef<string>` | Reflects by default |\n| `prop.bool(defaultValue?)` | `PropDef<boolean>` | Any non-null attribute value other than `\"false\"` parses as `true`; `\"false\"` or absent attribute is `false` |\n| `prop.number(defaultValue?)` | `PropDef<number>` | Returns default (not NaN) and warns in dev when attribute is not a valid number |\n| `prop.oneOf(allowed, defaultValue)` | `PropDef<T>` | Restricts to provided string union |\n| `prop.json(defaultValue)` | `PropDef<T>` | JSON.parse from attribute; `reflect: false` |\n| `prop.data<T>(defaultValue?)` | `PropDef<T>` | JS-only: never reads/writes an attribute; use for objects, arrays, callbacks, or any non-serialisable value |\n\n> **Choosing the right prop helper:**\n>\n> - **`prop.json`**: value can be declared in HTML (`<my-el config='{\"x\":1}'>`); attribute string is `JSON.parse`d.\n> - **`prop.data`**: value is always set from JavaScript (objects, arrays, callbacks, class instances); the attribute is never read. Use this for both data and function props.\n\nWhen you need custom parsing or `reflect: false`, use a raw `PropDef` object:\n\n```ts\nprops: {\n items: { default: [], parse: () => [], reflect: false },\n}\n```\n\nUse `prop.data` for props that hold JS-only values (including callbacks) that cannot be serialised through an HTML attribute:\n\n```ts\ndefine('data-grid', {\n props: {\n getRowKey: prop.data<(row: unknown) => string>(),\n columns: prop.data<DataGridColumn[]>([]),\n onSort: prop.data<(key: string) => void>(),\n },\n setup(props) {\n // Set from JS: grid.getRowKey = (row) => row.id\n return html`...`;\n },\n});\n```\n\n## Template and Directives\n\n### `html`\n\nTagged template literal that returns an `HTMLResult`. Supports text interpolation, ordinary attributes (`attr=`),\nboolean attributes (`?attr=`), events (`@event=`), refs (`ref=`), and nested templates.\n\n### `css`\n\nTagged template literal that returns a `CSSResult` for use in `styles`.\n\n### Directives\n\n| Directive | Purpose |\n| -------------------------------------- | ----------------------------------------------------------------------------------------------------- |\n| `each(source, key, render, fallback?)` | Keyed reactive list; render receives `Readable<T>` and `Readable<number>`; plain `T[]` is a one-time static snapshot |\n| `when(condition, truthy, falsy?)` | Conditional rendering |\n| `classMap(record)` | Reactive class string from object map |\n| `styleMap(record)` | Reactive inline style string from object map |\n| `live(signal)` | One-way binding that skips stale writes during active user input; use with `@input` handler |\n| `unsafeHtml(value)` | HTML rendering sink; sanitize untrusted values before calling |\n\n`each()` treats number and string keys as distinct. A duplicate-key update reports `ore:error` and preserves the last valid list.\n\n### `unsafeHtml`\n\n`unsafeHtml()` is an explicit HTML injection sink. It has no global sanitizer: sanitize untrusted\ncontent before passing it to the directive, so the trust boundary remains at the call site.\n\n```ts\nimport { unsafeHtml } from '@vielzeug/ore';\n\nconst safeArticle = sanitize(userSuppliedArticle);\n\nreturn html`<article>${unsafeHtml(safeArticle)}</article>`;\n```\n\n## Host Bindings\n\n`bind(config, options?)` is a plain function imported from `@vielzeug/ore`:\n\n```ts\nbind({\n attr: { role: 'button', 'aria-expanded': () => String(open.value) },\n class: { 'is-open': open },\n style: { '--height': () => height.value + 'px' },\n on: { click: handleClick },\n});\n```\n\n`bind()` auto-registers cleanup with the component scope: no manual `onCleanup` needed. Returns a cleanup function for early teardown.\n\n### Off-host bindings\n\nPass `{ target: el }` as a second argument to bind to any element other than the host:\n\n```ts\nbind(\n { attr: { 'aria-expanded': () => String(isOpen.value) } },\n { target: triggerEl },\n);\n```\n\nEvent listener options (`once`, `capture`, `passive`) are also accepted in the second argument. Cleanup is auto-registered with the component scope when called during setup.\n\n### Reactive ARIA attributes\n\nUse explicit `aria-*` keys in `bind({ attr }, { target })`:\n\n```ts\nconst stopAria = bind(\n {\n attr: {\n 'aria-controls': panelId,\n 'aria-expanded': () => isOpen.value,\n 'aria-haspopup': 'listbox',\n },\n },\n { target: triggerEl },\n);\n```\n\nStatic values are applied once. Getters and signals create reactive effects. Calling `stopAria()` stops updates. Values of `null`, `undefined`, or `false` remove the attribute.\n\n## Slots\n\n- `slots.has(name?)`: `Readable<boolean>`: whether the named (or default) slot has assigned content\n- `slots.elements(name?)`: `Readable<Element[]>`: the assigned elements for the slot\n\nSlot signals update reactively when assigned content changes, including when slots are inserted dynamically (via `when()` or `each()`) after mount.\n\n## Context API\n\n- `createContext<T>(description)`: Create a unique typed injection key; the description is diagnostic only\n- `provide(key, value)`: Provide a value to descendants\n- `inject(key)`: Resolve from nearest ancestor; returns `undefined` if not found\n- `inject(key, fallback)`: Resolve with a fallback value\n- `injectStrict(key)`: Resolve or throw if absent\n\n`provide()` and `inject()` must be called synchronously during `setup()`. Calling them outside a setup context throws\n`'Lifecycle hooks must be called during component setup'`. Context resolution walks the ancestor chain including shadow\nDOM boundaries. `inject()` resolves and caches its result once per consumer: provide a `Readable` (signal/computed)\nrather than a raw value if descendants need to observe later changes; re-calling `provide()` with a new raw value\nafterward is not seen by consumers that already resolved it (a dev-mode warning fires when a key is provided twice on\nthe same element). `provide()` registers cleanup automatically: context keys are removed from the registry when the\nproviding component disconnects, so reconnecting the same element runs `setup()` fresh without spurious \"overwriting\"\nwarnings or stale keys leaking to descendants.\n\n## Utilities\n\n- `ref<T>()`: Create a `Signal<T | null>` element reference. Set to the element via `ref=` in templates.\n- `createId(prefix = 'id')`: Generate a collision-resistant ID with a semantic prefix, a per-runtime tag, and a monotonic counter (for example, `'field-a3k21'`).\n\n## Form-Associated API\n\nImport from `@vielzeug/ore`.\n\n### `useField(options)`\n\nWire a form-associated element to `ElementInternals`. Requires `formAssociated: true` on the component definition. The `disabled` state tracking via `internals.states` (CustomStateSet) is skipped with a dev warning if the API is unavailable in the current environment.\n\n```ts\ntype FormFieldOptions<T> = {\n disabled?: Readable<boolean>;\n /**\n * When true, a null/undefined value is submitted as '' instead of null,\n * keeping the field's key present in FormData even when the value is absent.\n * Only applies to the default toFormValue; ignored if toFormValue is provided.\n * @default false\n */\n emptyStringForNull?: boolean;\n /** Called when the ancestor <form> resets (see onFormReset): restore local field state here. */\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n /** Recomputed reactively and passed straight to internals.setValidity(). null = always valid. */\n validationMessage?: Readable<string>;\n validity?: Readable<ValidityStateFlags | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity(): boolean;\n readonly internals: ElementInternals;\n reportValidity(): boolean;\n /** Set (non-empty message) or clear (empty string) a custom validity error. */\n setCustomValidity(message: string): void;\n};\n```\n\nPass `validity`/`validationMessage` to make `required`-style constraints participate in native constraint validation\nthrough `checkValidity()` and `reportValidity()`:\n\n```ts\nconst isBlank = (v: string) => v.trim() === '';\n\nuseField({\n validationMessage: computed(() => (required.value && isBlank(value.value) ? 'This field is required.' : '')),\n validity: computed(() => (required.value && isBlank(value.value) ? { valueMissing: true } : null)),\n value,\n});\n```\n\n## Testing APIs\n\nImport from `@vielzeug/ore/testing`.\n\n| API | Purpose |\n| ------------------------ | ------------------------------------------------------------------------------------------ |\n| `mount(setup, options?)` | Mount a component and return a test fixture |\n| `cleanup()` | Remove all mounted elements and reset test state |\n| `install(afterEach, options?)` | Register auto-cleanup; pass `{ formInternals: true }` to also install the `ElementInternals`/`FormData`/`<form>.reset()` jsdom polyfill (see below) |\n| `installFormInternalsPolyfill()` | Installs the form-internals polyfill directly (returns an `uninstall()` that restores every patched global). Usually called via `install(afterEach, { formInternals: true })` |\n| `flush(options?)` | Drain reactive updates and animation frames |\n| `renderHook(setup)` | Run lifecycle hooks in isolation; overload accepts `propDefs` as first arg for typed props |\n| `OreTimeoutError` | Error thrown when `flush()` cannot settle tracked Ore work |\n\n> **Test isolation:** `cleanup()` removes mounted elements and resets Ore's cross-test caches and counters. Call it in `afterEach`, or use `install(afterEach)`.\n\nImport `within`, named dispatchers such as `fireClick`, and waits such as `waitUntil` or `waitForEvent` from\n`@vielzeug/assay`.\n\n> **Form-associated component testing:** jsdom implements none of the `ElementInternals` form-association API: `install(afterEach, { formInternals: true })` polyfills `setFormValue`/`setValidity`/`checkValidity`/`reportValidity`/`validationMessage`/`validity`/`states`, mixes `checkValidity`/`reportValidity`/`validity`/`validationMessage` onto the host element itself (real browsers do this for any `formAssociated: true` element), makes `FormData` collect a form-associated element's set value, and makes `<form>.reset()` invoke `formResetCallback()`. Every patch is a guarded no-op when its target already exists, and `installFormInternalsPolyfill()` returns an `uninstall()` that restores every patched global. The polyfill is opt-in (`{ formInternals: true }`) because the patches are global: suites without form-associated components shouldn't carry them. A downstream package (e.g. a component library built on `ore`) should rely on this instead of hand-rolling its own copy.\n\n#### `Fixture` interface\n\n```ts\ninterface Fixture<T extends HTMLElement = HTMLElement> {\n [Symbol.dispose](): void; // Delegates to dispose(): enables `using` declarations\n element: T;\n readonly disposed: boolean; // true after dispose() has been called\n readonly shadow: ShadowRoot | null;\n get<E extends Element>(selector: string): E;\n query<E extends Element>(selector: string): E | null;\n queryAll<E extends Element>(selector: string): E[];\n getByText<E extends Element>(text: string, selector?: string): E;\n queryByText<E extends Element>(text: string, selector?: string): E | null;\n queryAllByText<E extends Element>(text: string, selector?: string): E[];\n getByTestId<E extends Element>(testId: string): E;\n queryByTestId<E extends Element>(testId: string): E | null;\n queryAllByTestId<E extends Element>(testId: string): E[];\n attr(name: string, value: string | number | boolean): Promise<void>;\n attrs(record: Record<string, string | number | boolean>): Promise<void>;\n flush(options?: FlushOptions): Promise<void>;\n act(fn: () => unknown): Promise<void>;\n dispose(): void; // Removes the component from the DOM: idempotent\n}\n```\n\n#### `renderHook`\n\nUseful for testing composable lifecycle hooks (`onMounted`, `watchEffect`, `inject`, etc.) without a template. `onMounted`/`onCleanup`/`watchEffect`/... work exactly as inside a real `setup()`, since they resolve the same implicit current-component context:\n\n```ts\n// Without props\nconst { result, flush, dispose } = await renderHook(() => {\n const count = signal(0);\n onMounted(() => {\n count.value = 1;\n });\n return count;\n});\nexpect(result.value).toBe(1);\n\n// With typed props (prop-defs overload)\nconst { result } = await renderHook({ label: prop.string('hello'), count: prop.number(0) }, (props) => props.label);\nexpect(result.value).toBe('hello');\n```\n\n## Ripple Primitives\n\nOre does **not** re-export reactive primitives. Import them directly from `@vielzeug/ripple`:\n\n```ts\nimport { batch, computed, signal, watch } from '@vielzeug/ripple';\n```\n\nSee the [Ripple documentation](/ripple/) for the full API.\n\n## Lifecycle Events\n\n| Event | When |\n| ------------- | ------------------------------------------------------------------------- |\n| `ore:error` | When a lifecycle callback fails: bubbles, composed; detail is `OreLifecycleError` |\n\n## Types\n\n```ts\ntype PropDef<T> = {\n readonly default: T;\n readonly parse: (value: string | null) => T;\n reflect?: boolean;\n};\n\ntype PropsDef<T extends Record<string, unknown>> = {\n [K in keyof Required<T>]: PropDef<T[K & keyof T]>;\n};\n\ntype PropInputDefs = Record<string, PropDef<unknown>>;\n\n/**\n * Infer reactive props type from a PropInputDefs map.\n * Each entry becomes Readable<T> keyed by prop name.\n */\ntype InferProps<D extends PropInputDefs> = {\n readonly [K in keyof D]-?: Readable<InferPropValue<D[K]>>;\n};\n\n// Runtime hooks: all plain functions imported from '@vielzeug/ore', not fields on an object.\ntype OnMountedCallback = () => Cleanup | undefined;\ntype OnFormResetCallback = () => void;\n\ndeclare function onMounted(fn: OnMountedCallback): void; // DOM-ready callback; runs after each connection's render\ndeclare function onCleanup(fn: Cleanup): void; // Register teardown; called on disconnect\ndeclare function onElement<T extends HTMLElement>(\n ref: Readable<T | null>,\n callback: (el: T) => Cleanup | undefined,\n): () => void;\ndeclare function onEvent<K extends keyof HTMLElementEventMap>(\n target: EventTarget | null | undefined,\n event: K,\n listener: (e: HTMLElementEventMap[K]) => void,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onEvent(\n target: EventTarget | null | undefined,\n event: string,\n listener: EventListener,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onFormReset(fn: OnFormResetCallback): void; // Runs on every ancestor <form> reset; formAssociated only\ndeclare function watchEffect(fn: () => Cleanup | undefined): () => void; // Scoped reactive effect; auto-cleaned on disconnect\ndeclare function bind(config: HostBindConfig, options?: BindOptions): () => void; // Bindings for host or any target element\ndeclare function provide<T>(key: InjectionKey<T>, value: T): void; // Register a context value on the host element\ndeclare function inject<T>(key: InjectionKey<T>): T | undefined;\ndeclare function inject<T>(key: InjectionKey<T>, fallback: T): T;\ndeclare function getHost(): HTMLElement; // The current component's host element\ndeclare function useEmit<Emits extends Record<string, unknown> = Record<string, never>>(): EmitFn<Emits>;\ndeclare function useSlots<SlotNames extends string = string>(): ComponentSlots<SlotNames>;\n\ntype ComponentDefinition<Props extends Record<string, unknown> = Record<never, never>> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n\ntype MaybeReactive<T> = T | Readable<T> | (() => T); // Plain value, signal, or getter: accepted by when(), each(), classMap(), and bind()\n\ntype HostBindingValue = MaybeReactive<string | number | boolean | null | undefined>;\n\ntype AttributeBindings = Record<string, HostBindingValue>;\n\ntype HostBindConfig = {\n attr?: AttributeBindings;\n class?: (() => Record<string, boolean>) | Record<string, Readable<boolean> | (() => boolean) | boolean>;\n on?: Record<string, ((event: Event) => void) | undefined>;\n style?: Record<string, HostBindingValue>;\n};\n\ntype BindOptions = AddEventListenerOptions & {\n target?: Element;\n};\n\ntype ComponentSlots<S extends string = string> = {\n elements(name?: S): Readable<Element[]>;\n has(name?: S): Readable<boolean>;\n};\n\ntype Ref<T extends Element> = Signal<T | null>;\n\ntype RefCallback<T extends Element> = (el: T | null) => void;\n\ntype InjectionKey<T> = symbol & { readonly __ore_injection_key?: T };\n\ninterface HTMLResult {\n mount(\n parent: ParentNode,\n anchor: Node | null,\n registerCleanup: (fn: () => void) => void,\n ): Node[];\n}\n\ntype CSSResult = {\n content: string;\n toString(): string;\n};\n\ntype LiveBinding<T> = { readonly source: Readable<T> };\n\ntype EmitFn<T extends Record<string, unknown>> = {\n <K extends KeysWithoutDetail<T>>(event: K): boolean;\n <K extends Exclude<keyof T, KeysWithoutDetail<T>>>(event: K, detail: T[K]): boolean;\n};\n// KeysWithoutDetail is an internal helper type, not exported.\n\ntype FormFieldOptions<T = unknown> = {\n disabled?: Readable<boolean>;\n emptyStringForNull?: boolean;\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n validationMessage?: Readable<string>;\n validity?: Readable<ValidityStateFlags | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity: () => boolean;\n readonly internals: ElementInternals;\n reportValidity: () => boolean;\n setCustomValidity: (message: string) => void;\n};\n\n/** Phase in which a OreError occurred. */\ntype OreErrorPhase = 'each-reconcile' | 'form-reset' | 'mounted' | 'setup';\n```\n\n## Errors\n\n`OreError` is the base class for every Ore error class: `err instanceof OreError` catches all of them.\n\n- **`OreApiError`**: thrown when the `ore` API itself is misused: calling `define()` with a duplicate tag, calling a lifecycle hook (`inject`, `onMounted`, `onCleanup`, `onEvent`, …) outside of `setup()`, or passing an invalid prop definition to `define()`.\n- **`OreInternalError`**: thrown when an Ore invariant fails, indicating a package bug rather than invalid application code.\n- **`OreLifecycleError`**: reported in the `ore:error` event when component `setup()`, a mounted callback, a form-reset callback, or `each()` reconciliation fails. Extends `OreError` with:\n - `component: string`: the element's local name\n - `phase: OreErrorPhase`: `'setup'` | `'mounted'` | `'form-reset'` | `'each-reconcile'`\n - `cause: Error`: the original error thrown by `setup()`\n- **`OreTimeoutError`**: thrown by `flush()` (from `@vielzeug/ore/testing`) when pending Ore work does not settle before its timeout.\n\nLifecycle failures dispatch a bubbling, composed `ore:error` event whose `detail` is the `OreLifecycleError`. Setup\nfailures still rethrow their original error; mounted and form-reset callback failures are reported through the same\nevent so their remaining callbacks can continue.\n",
|
|
6
|
-
"usage": "---\ntitle: 'Ore: Usage Guide'\ndescription: Practical Ore usage patterns for components, props, templates, slots, context, forms, Sentinel integration, and tests.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`define(tag, definition)` creates and registers a custom element. Call it from the module or browser bootstrap responsible for registration.\n\nYour `setup()` function receives typed prop signals and returns an `HTMLResult` directly. Its state belongs to the\ncurrent connection: disconnect disposes it, and reconnecting the same element runs setup again.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('status-chip', {\n setup() {\n const online = signal(true);\n\n return html`\n <button @click=${() => (online.value = !online.value)}>${() => (online.value ? 'Online' : 'Offline')}</button>\n `;\n },\n});\n```\n\nEverything besides `props`: lifecycle hooks, host bindings, context, slots, emit: is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, getHost, html, bind, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-widget', {\n setup(_props) {\n const el = getHost(); // the host HTMLElement\n const emit = useEmit<{ close: undefined }>(); // typed event emitter\n const slots = useSlots<'header'>(); // reactive slot observation\n\n bind({ attr: { role: 'group' } }); // host binding helper (attr, class, style, on)\n\n return html`<slot></slot>`;\n },\n});\n```\n\n## signals and effects\n\nOre does not re-export ripple primitives: import them directly from `@vielzeug/ripple`.\n\n```ts\nimport { batch, computed, effect, signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst doubled = computed(() => count.value * 2);\n\neffect(() => {\n console.log('doubled =', doubled.value);\n});\n\nwatch(count, (next, prev) => {\n console.log('count changed', prev, '->', next);\n});\n\nbatch(() => {\n count.value = 1;\n count.value = 2;\n});\n```\n\n## onMounted and lifecycle\n\nUse `onMounted()` for DOM-dependent initialization that must run after the template is mounted. Use `onElement(ref, cb)` for work tied to a specific DOM node. `onEvent()` attaches a listener that is automatically removed on disconnect.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, onElement, onEvent, onMounted, ref, useSlots } from '@vielzeug/ore';\n\ndefine('deferred-init', {\n setup(_props) {\n const tabIndex = signal(0);\n const inputRef = ref<HTMLInputElement>();\n const slots = useSlots<'items'>();\n\n onMounted(() => {\n const items = slots.elements('items').value;\n console.log('Found', items.length, 'items');\n });\n\n onElement(inputRef, (input) => {\n input.focus();\n });\n\n onEvent(window, 'keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') tabIndex.value = 0;\n });\n\n return html`<div><slot name=\"items\"></slot><input ref=${inputRef} /></div>`;\n },\n});\n```\n\n## prop definitions\n\nUse `prop.*` helpers for common cases, or raw `PropDef` objects for custom parsing or `reflect: false`.\n\n```ts\nimport { define, html, prop } from '@vielzeug/ore';\n\ndefine('x-button', {\n props: {\n label: prop.string('Button'),\n disabled: prop.bool(false),\n variant: prop.oneOf(['primary', 'secondary'] as const, 'primary'),\n count: prop.number(0),\n },\n setup(props) {\n return html`\n <button ?disabled=${props.disabled} data-variant=${props.variant}>${props.label} (${props.count})</button>\n `;\n },\n});\n```\n\n## template bindings\n\n`html` supports text, attributes, booleans, properties, events, refs, and nested templates.\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { define, html, ref } from '@vielzeug/ore';\n\ndefine('profile-name', {\n setup() {\n const name = signal('Alice');\n const inputRef = ref<HTMLInputElement>();\n\n return html`\n <label title=${computed(() => 'Current: ' + name.value)}>Name</label>\n <input\n ref=${inputRef}\n value=${name}\n aria-label=${() => 'Current name ' + name.value}\n @input=${(event: Event) => {\n name.value = (event.target as HTMLInputElement).value;\n }} />\n <p>Hello ${name}</p>\n `;\n },\n});\n```\n\n## directives\n\nOre exports `each`, `classMap`, `styleMap`, `when`, `live`, and `unsafeHtml` from `@vielzeug/ore`. Use ordinary\nattribute bindings plus native event handlers for two-way input state; no special model directive is required.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { classMap, define, each, html, styleMap, when } from '@vielzeug/ore';\n\ndefine('task-list', {\n setup() {\n const tasks = signal([{ id: 1, text: 'Write tests' }]);\n const active = signal(true);\n\n return html`\n <ul\n class=\"${classMap({ ready: () => tasks.value.length > 0 })}\"\n style=${styleMap({ opacity: () => (active.value ? 1 : 0.5) })}>\n ${when(\n () => active.value,\n () => html`<li>Active</li>`,\n () => html`<li>Paused</li>`,\n )}\n ${each(\n tasks,\n (task) => task.id,\n (task) => html`<li>${() => task.value.text}</li>`,\n )}\n </ul>\n `;\n },\n});\n```\n\n### each() API\n\n`each(source, key, render, fallback?)` takes positional arguments:\n\n- **source**: signal, getter, or plain array\n- **key**: function returning a unique string or number per item; number and string keys remain distinct\n- **render**: receives reactive `item` and `index` signals\n- **fallback**: optional, rendered when the list is empty\n\n```ts\neach(\n items,\n (item) => item.id,\n (item, index) => html`<li>#${index}: ${() => item.value.label}</li>`,\n () => html`<li>No items</li>`,\n);\n```\n\n## live form bindings\n\nUse `live(signal)` for inputs that should preserve in-progress user edits instead of overwriting the DOM on stale writes.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, live } from '@vielzeug/ore';\n\ndefine('live-search', {\n setup() {\n const query = signal('');\n\n return html`\n <input value=${live(query)} @input=${(e: Event) => (query.value = (e.target as HTMLInputElement).value)} />\n `;\n },\n});\n```\n\n## host bindings\n\n`bind()` wires reactive attrs, classes, styles, and events to the host element.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html } from '@vielzeug/ore';\n\ndefine('x-toggle', {\n setup(_props) {\n const open = signal(false);\n\n bind({\n attr: { 'aria-expanded': () => String(open.value), role: 'button', tabindex: 0 },\n class: { 'is-open': open },\n on: { click: () => (open.value = !open.value) },\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nThe `bind` config supports `attr`, `class`, `style`, and `on` sections.\n\n## ARIA bindings\n\nUse explicit `aria-*` keys in `bind({ attr: config }, { target })` to reactively sync ARIA attributes to any element.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onMounted } from '@vielzeug/ore';\n\ndefine('x-disclosure', {\n setup(_props) {\n const open = signal(false);\n const panelId = 'disclosure-panel';\n\n bind({\n attr: { role: 'button', tabindex: 0 },\n on: { click: () => (open.value = !open.value) },\n });\n\n onMounted(() => {\n const trigger = document.querySelector('#trigger') as HTMLElement;\n if (trigger) {\n // bind() registers cleanup automatically when called inside setup\n bind(\n {\n attr: {\n 'aria-controls': panelId,\n 'aria-expanded': () => String(open.value),\n 'aria-haspopup': 'region',\n },\n },\n { target: trigger },\n );\n }\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nStatic values are applied once. Getter functions create reactive effects. Setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n`bind()` always returns a cleanup function. Use it to stop syncing early when a trigger element can be swapped out:\n\n```ts\nonMounted(() => {\n const trigger = document.querySelector('#trigger') as HTMLElement;\n const stopAria = bind({ attr: { 'aria-expanded': () => String(open.value) } }, { target: trigger });\n\n // Stop syncing when the trigger is replaced\n onCleanup(stopAria);\n});\n```\n\n### Binding a non-host element with `bind()`\n\nPass `{ target: el }` as a second argument to bind attributes, classes, styles, or events to any element:\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onMounted, ref } from '@vielzeug/ore';\n\ndefine('button-wrapper', {\n setup(_props) {\n const visible = signal(false);\n const btnRef = ref<HTMLButtonElement>();\n\n onMounted(() => {\n const btn = btnRef.value;\n if (!btn) return;\n\n bind(\n {\n attr: { 'aria-pressed': () => String(visible.value) },\n on: { click: () => (visible.value = !visible.value) },\n },\n { target: btn },\n );\n });\n\n return html`<button ref=${btnRef}>Toggle</button>`;\n },\n});\n```\n\n## slots and emits\n\n```ts\nimport { define, html, useEmit, useSlots, when } from '@vielzeug/ore';\n\ndefine('card-with-footer', {\n setup(_props) {\n const slots = useSlots<'header' | 'footer'>();\n const emit = useEmit<{ action: undefined }>();\n\n return html`\n <div class=\"card\">\n <slot name=\"header\"></slot>\n <slot></slot>\n ${when(slots.has('footer'), () => html`<footer><slot name=\"footer\"></slot></footer>`)}\n </div>\n <button @click=${() => emit('action')}>Go</button>\n `;\n },\n});\n```\n\nPass a `SlotNames` type parameter to `useSlots<SlotNames>()` to get typed `slots.has()` and `slots.elements()` calls.\n\n## context provide/inject\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { createContext, define, html, injectStrict, provide } from '@vielzeug/ore';\n\nconst COUNT_CTX = createContext<ReturnType<typeof signal<number>>>('count');\n\ndefine('count-provider', {\n setup(_props) {\n const count = signal(0);\n provide(COUNT_CTX, count);\n\n return html`<button @click=${() => count.value++}><slot></slot></button>`;\n },\n});\n\ndefine('count-consumer', {\n setup() {\n const count = injectStrict(COUNT_CTX);\n\n return html`<p>Count: ${count}</p>`;\n },\n});\n```\n\n`provide()` registers cleanup automatically: context keys are removed from the registry when the providing component disconnects. On reconnect, `setup()` runs fresh and `provide()` re-registers without spurious \"overwriting\" warnings. Provide a `Readable` (signal/computed) rather than a raw value if descendants need to observe later changes: `inject()` resolves and caches the value once per consumer connection.\n\n## form-associated elements\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, prop } from '@vielzeug/ore';\nimport { useField } from '@vielzeug/ore';\n\ndefine('rating-input', {\n formAssociated: true,\n setup() {\n const value = signal(0);\n const field = useField({ value });\n\n return html`\n <button @click=${() => (value.value = 1)}>1</button>\n <button @click=${() => (value.value = 2)}>2</button>\n <button @click=${() => (value.value = 3)}>3</button>\n <button @click=${() => field.reportValidity()}>Validate</button>\n <p>Current: ${value}</p>\n `;\n },\n});\n```\n\n## Sentinel Observers\n\nUse `@vielzeug/sentinel` for subscribable browser and DOM observations. Create element-dependent Sentinels inside `onMounted()` and dispose them with the component.\n\n```ts\nimport { define, html, onCleanup, onMounted, ref, watchEffect } from '@vielzeug/ore';\nimport { fromSubscribable } from '@vielzeug/ripple';\nimport { createElementSize, SentinelUnavailableError } from '@vielzeug/sentinel';\n\ndefine('x-observed', {\n setup(_props) {\n const boxRef = ref<HTMLDivElement>();\n\n onMounted(() => {\n const element = boxRef.value;\n if (!element) return;\n\n try {\n const size = createElementSize(element);\n const sizeState = fromSubscribable(size, { signal: size.disposalSignal });\n\n watchEffect(() => {\n console.log(sizeState.value?.width);\n });\n\n onCleanup(() => size.dispose());\n } catch (error) {\n if (!(error instanceof SentinelUnavailableError)) throw error;\n }\n });\n\n return html`<div ref=${boxRef}>Observe me</div>`;\n },\n});\n```\n\n## testing utilities\n\nImport from `@vielzeug/ore/testing`.\n\n```ts\nimport { afterEach, describe, expect, it } from 'vitest';\nimport { signal } from '@vielzeug/ripple';\nimport { fireClick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { cleanup, mount } from '@vielzeug/ore/testing';\n\ndescribe('my-counter', () => {\n afterEach(cleanup);\n\n it('increments on click', async () => {\n let count!: ReturnType<typeof signal<number>>;\n const { query, act } = await mount(() => {\n count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n });\n\n expect(query('button')?.textContent).toBe('0');\n\n await act(() => fireClick(query('button')!));\n\n expect(query('button')?.textContent).toBe('1');\n });\n});\n```\n\n## Framework Integration\n\nOre components are standard custom elements and work natively in any framework.\n\n::: code-group\n\n```tsx [React]\n// React 19+ supports custom elements natively.\nimport './x-toggle'; // wherever define('x-toggle', { ... }) is called\n\nfunction App() {\n return <x-toggle aria-label=\"Open menu\" />;\n}\n```\n\n```ts [Vue 3]\n<script setup lang=\"ts\">\nimport './x-toggle'; // wherever define('x-toggle', { ... }) is called\nimport { ref } from 'vue';\n\nconst open = ref(false);\n</script>\n\n<template>\n <x-toggle :aria-label=\"'Open menu'\" @click=\"open = !open\" />\n</template>\n```\n\n```svelte [Svelte]\n<script>\n import './x-toggle'; // wherever define('x-toggle', { ... }) is called\n\n function handleClick() {\n console.log('toggled');\n }\n</script>\n\n<x-toggle aria-label=\"Open menu\" on:click={handleClick} />\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Ripple\n\nImport ripple primitives directly from `@vielzeug/ripple` for standalone reactive state outside components.\n\n```ts\nimport { signal, computed } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\n// Shared state created outside any component\nconst theme = signal<'light' | 'dark'>('light');\nconst isDark = computed(() => theme.value === 'dark');\n\ndefine('theme-toggle', {\n setup() {\n return html`\n <button @click=${() => (theme.value = isDark.value ? 'light' : 'dark')}>\n ${() =>\n isDark.value ? '<ore-icon name=\"sun\" size=\"16\"></ore-icon>' : '<ore-icon name=\"moon\" size=\"16\"></ore-icon>'}\n </button>\n `;\n },\n});\n```\n\n### With Forge\n\nUse `@vielzeug/forge` for typed form state. `useField()` remains intentionally narrow: it connects a form-associated\ncustom element to native `ElementInternals` without imposing submission, validation, or dirty-state policy.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('signup-form', {\n setup(_props) {\n const form = createForm({ initialValues: { email: '' } });\n\n return html`\n <form\n @submit=${(event: SubmitEvent) => {\n event.preventDefault();\n void form.submit(async (values) => {\n console.log(values);\n });\n }}>\n <slot></slot>\n </form>\n `;\n },\n});\n```\n\n## Best Practices\n\n- Setup returns `html\\`...\\`` directly: not a function wrapping the template.\n- Use `watchEffect()` for reactive subscriptions tied to component lifetime: it auto-registers cleanup on disconnect.\n- Use `onElement(ref, cb)` instead of `onMounted` when the work is tied to a single DOM node.\n- Bind host attributes and classes via `bind()` rather than mutating the element directly.\n- Provide context at the nearest ancestor: avoid global context singletons.\n- Call `onCleanup()` for every resource allocated in `setup()` (WebSockets, intervals, external subscriptions).\n- Use `live(signal)` for form inputs to prevent clobbering user-in-progress edits.\n- Extract composable helper functions freely: `onMounted`/`onCleanup`/`bind`/... resolve the active component through implicit context, so they work from any function called (transitively) during `setup()`, with no need to pass them in as parameters.\n- Test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic DOM events, queries, and waits\n from `@vielzeug/assay`.\n",
|
|
4
|
+
"index": "---\ntitle: 'Ore: Web component authoring with signals'\ndescription: Functional custom-element authoring with typed props, reactive templates, lifecycle helpers, and testing utilities.\npackage: ore\ncategory: ui-primitives\nkeywords: [web-components, custom-elements, reactive, templates, signals, lifecycle]\nrelated: [ripple, refine, orbit]\nexports: [define, prop, html, css, ref, createContext, inject, injectStrict, provide, onMounted, onCleanup, onEvent, onElement, onFormReset, watchEffect, useEmit, useSlots, getHost, bind, each, when, classMap, styleMap, live, model, unsafeHtml, useField, createId, OreError, OreApiError, OreInternalError, OreLifecycleError, AttributeBindings, BindOptions, MaybeReactive]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ore\" />\n\n## Why Ore?\n\nOre keeps custom elements functional and signal-driven while giving you direct control over templates, lifecycle hooks, host bindings, and form-associated behavior.\n\n```ts\n// Before: vanilla custom element boilerplate\nclass MyCounter extends HTMLElement {\n #count = 0;\n connectedCallback() {\n this.attachShadow({ mode: 'open' });\n this.#render();\n }\n #render() {\n this.shadowRoot!.innerHTML = `<button>${this.#count}</button>`;\n this.shadowRoot!.querySelector('button')!.onclick = () => {\n this.#count++;\n this.#render();\n };\n }\n}\ncustomElements.define('my-counter', MyCounter);\n\n// After: Ore\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('my-counter', {\n setup() {\n const count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n },\n});\n```\n\n| Feature | Ore | Lit | Stencil |\n| -------------------------- | ------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"ore\" type=\"size\" /> | ~12 kB | ~60 kB+ toolchain |\n| Signal-first runtime | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> (separate signals package) | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Functional component setup | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed prop helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Host binding helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | Partial |\n| Form-associated helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Partial |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Ore when** you want typed, signal-driven custom elements with minimal runtime overhead and no framework lock-in.\n\n**Consider Lit when** you need a mature ecosystem with wide community adoption and don't need signal-based reactivity.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ore @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ore @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ore @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { bind, css, define, html, onMounted, prop } from '@vielzeug/ore';\n\ndefine('my-counter', {\n props: {\n label: prop.string('Count'),\n step: prop.number(1),\n },\n styles: [\n css`\n :host {\n display: inline-grid;\n gap: 0.5rem;\n }\n `,\n ],\n setup(props) {\n const count = signal(0);\n const doubled = computed(() => count.value * 2);\n\n bind({ class: { 'is-positive': () => count.value > 0 } });\n\n onMounted(() => console.log('mounted'));\n\n return html`\n <button @click=${() => (count.value += props.step.value)}>${props.label}: ${count}</button>\n <p>Doubled: ${doubled}</p>\n `;\n },\n});\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Signal-first runtime with `signal`, `computed`, `watch`, `batch` from `@vielzeug/ripple`: import them directly\n- Functional component authoring via `define(tag, { props, setup, styles, formAssociated })`\n- Props via `prop.*` helpers (`prop.string`, `prop.number`, `prop.bool`, `prop.oneOf`, `prop.json`, `prop.data`) or raw `PropDef` objects\n- `setup(props)` takes only props and returns an `HTMLResult` directly: `return html\\`...\\``\n- Lifecycle hooks: `onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`: plain functions imported from `@vielzeug/ore`, called directly from `setup()` or any composable it calls\n- Directives: `each` (transactional keyed list reconciliation), `classMap`, `styleMap`, `when`, `live`, `model`, `unsafeHtml`\n- Host bindings via `bind({ attr, class, style, on })`: pass `{ target: el }` to bind any off-host element\n- Reactive ARIA sync via explicit `aria-*` keys in `bind({ attr }, { target })`, with automatic cleanup on disconnect\n- Context via `provide(key, value)` / `inject(key)`; typed emit/slots via `useEmit<Emits>()` / `useSlots<SlotNames>()`\n- Form-associated `useField()` and observer helpers are root exports\n- Testing utilities (`@vielzeug/ore/testing`): `mount`, `renderHook`, `flush`, `cleanup`\n- Generic testing utilities (scoped queries, named event dispatchers, and async waits) are exported by `@vielzeug/assay`\n\n</div>\n\n## Package Entry Points\n\n| Import | Purpose |\n| --------------------------- | ----------------------------------------------------------------------------- |\n| `@vielzeug/ore` | All browser runtime APIs: components, directives, `useField`, and observers |\n| `@vielzeug/ore/testing` | Ore-specific mounting, lifecycle flushing, hooks, cleanup, and form internals |\n| `@vielzeug/assay` | Generic DOM events, scoped queries, and async waiting |\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](../refine/index.md) for prebuilt accessible components powered by Ore.\n- [Ripple](../ripple/index.md) for reactive state used inside Ore components.\n- [Forge](../forge/index.md) for typed form state that integrates with Ore.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: 'Ore: API Reference'\ndescription: Complete API reference for @vielzeug/ore and @vielzeug/ore/testing.\n---\n\n[[toc]]\n\n## API Overview\n\nAll browser-runtime symbols below are imported from `@vielzeug/ore`. Lifecycle/context/binding functions (`onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`, `bind`, `provide`, `useEmit`, `useSlots`, `getHost`) resolve the active component through an implicit \"current component\" context: they work when called synchronously during `setup()`, or from any composable function `setup()` calls (transitively), but throw if called outside that window.\n\n> `watchEffect` is not named `watch`: `@vielzeug/ripple` already exports a `watch(source, callback)` with different semantics (explicit source + old/new value pair), and the two are frequently imported in the same file.\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ---------------------- | ----------------------------------------------------- | -------------- | -------------------------------------------------------------------------- |\n| `define()` | Create and register a custom element | Sync | Tag must contain a hyphen; call before first use |\n| `html` | Tagged template literal returning HTMLResult | Sync | Expressions must be signals, functions, or primitives |\n| `prop.*` | Typed prop helpers (string, bool, number, …) | Sync | Prop values are signals: read `.value` |\n| `provide()`/`inject()` | Context API for parent-to-descendant sharing | Setup only | Must be called synchronously during `setup()` |\n| `ref()` | Reactive reference to a DOM element | Sync | Value is null until after first mount |\n| `createContext()` | Create a unique typed injection key | Sync | Context is scoped to the component tree |\n| `each()` | Keyed list rendering with DOM diffing | Sync | Duplicate keys report `ore:error`; plain `T[]` is a one-time static render |\n| `when()` | Conditional branch rendering | Sync | Getter-fn computed disposed on cleanup; static bool skips subscription |\n| `live(signal)` | One-way binding that skips stale writes during input | Sync | Use for controlled inputs alongside a manual `@input` handler |\n| `model(signal)` | Two-way form-control binding (value/checked + write-back) | Sync | Plain `value`/`checked` attribute only; pass `get` to keep numeric signals numeric |\n| `onMounted(fn)` | DOM-ready callback | Setup only | Must be called synchronously during `setup()` |\n| `onCleanup(fn)` | Register teardown | Setup only | Called on component disconnect |\n| `onEvent(target, …)` | Scoped event listener with auto-cleanup | Setup only | No-ops on null target; removed on disconnect |\n| `useField(options)` | Wire signal to form `ElementInternals` | Setup only | Requires `formAssociated: true` on the component definition |\n| `onFormReset(fn)` | Run work when the ancestor `<form>` resets | Setup only | Fires every reset (not one-shot); only for `formAssociated: true` components |\n| `useEmit<Emits>()` | Typed `emit()` bound to the current host | Setup only | Call once per component; returns `dispatchEvent`'s boolean (`false` if a listener called `preventDefault()`) |\n| `useSlots<SlotNames>()`| Reactive slot presence/element signals | Setup only | Safe to call more than once: the underlying registry is created once |\n| `getHost()` | The current component's host element | Setup only | Prefer a higher-level helper (`bind`, …) when one exists |\n\n## Package Entry Points\n\n| Import | Purpose |\n| ------------------------- | ------------------------------------------------------------------ |\n| `@vielzeug/ore` | All browser runtime APIs, including directives, fields, and lifecycle helpers |\n| `@vielzeug/ore/testing` | Ore-specific mounting, lifecycle, hook, cleanup, and form test support |\n| `@vielzeug/assay` | Generic DOM events, scoped queries, and async waiting |\n\n## Core Component API\n\n### `define(tag, definition)`\n\n```ts\ndefine<Props>(tag: string, definition: ComponentDefinition<Props>): CustomElementConstructor;\n```\n\nReturns the registered class (for `instanceof` or subclassing); registration itself is the only global side effect.\n\nThe `setup()` function receives only typed prop signals:\n\n```ts\nsetup(props) {\n return html`<div>${props.label}</div>`;\n}\n```\n\nEverything else: lifecycle hooks, host bindings, context, slots, emit: is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, html, onMounted, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-card', {\n setup(_props) {\n const emit = useEmit<{ close: undefined }>();\n const slots = useSlots<'header' | 'footer'>();\n\n onMounted(() => console.log('mounted'));\n\n // emit() returns dispatchEvent's boolean: false if a listener called preventDefault()\n const notCancelled = emit('close');\n\n return html`${when(slots.has('header'), () => html`<slot name=\"header\"></slot>`)}`;\n },\n});\n```\n\n`useEmit<Emits>()` and `useSlots<SlotNames>()` are factory hooks: call them once per setup run to get a typed\n`emit`/`slots` bound to the current host. `useSlots()` is safe to call more than once within that setup run.\n\n### ComponentDefinition\n\n```ts\ntype ComponentDefinition<Props> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM (no shadow root)\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n```\n\n## Runtime Helpers\n\n`onMounted`, `onCleanup`, `onEvent`, `onElement`, and `watchEffect` are plain functions imported from `@vielzeug/ore`. Call them directly during `setup()`.\n\n```ts\nimport { html, onCleanup, onEvent, onMounted } from '@vielzeug/ore';\n\nsetup(props) {\n onMounted(() => {\n // DOM is ready; return a function for mount-scoped cleanup\n return () => { /* cleanup on unmount */ };\n });\n\n onCleanup(() => { /* called on disconnect */ });\n\n onEvent(window, 'keydown', (e) => { /* auto-removed on disconnect */ });\n\n return html`...`;\n}\n```\n\nBecause these resolve the active component through an implicit context (rather than a value threaded through parameters), composable helper functions can call them directly too: no need to pass hooks in as options:\n\n```ts\nimport { onCleanup } from '@vielzeug/ore';\n\nfunction useMyHelper() {\n onCleanup(() => { /* teardown */ });\n}\n\n// In setup:\nsetup(_props) {\n useMyHelper();\n return html`...`;\n}\n```\n\n## Props API\n\n| Helper | Signature | Notes |\n| ----------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------ |\n| `prop.string(defaultValue?)` | `PropDef<string>` | Reflects by default |\n| `prop.bool(defaultValue?)` | `PropDef<boolean>` | Any non-null attribute value other than `\"false\"` parses as `true`; `\"false\"` or absent attribute is `false` |\n| `prop.number(defaultValue?)` | `PropDef<number>` | Returns default (not NaN) and warns in dev when attribute is not a valid number |\n| `prop.oneOf(allowed, defaultValue?)` | `PropDef<T>` | Restricts to a fixed set of string or number members; `parse` falls back to `defaultValue` (or `undefined` when omitted) for anything outside the set. Numeric members match the attribute parsed as a number |\n| `prop.json(defaultValue)` | `PropDef<T>` | JSON.parse from attribute; `reflect: false` |\n| `prop.data<T>(defaultValue?)` | `PropDef<T>` | JS-only: never reads/writes an attribute; use for objects, arrays, callbacks, or any non-serialisable value |\n\n> **Choosing the right prop helper:**\n>\n> - **`prop.oneOf`**: the honest way to type a union-valued prop: the restriction is enforced at runtime (`parse` falls back for anything outside the set), not just in a type parameter a raw attribute value could violate. Pass an `as const` array of the allowed string or number members.\n> - **`prop.json`**: value can be declared in HTML (`<my-el config='{\"x\":1}'>`); attribute string is `JSON.parse`d.\n> - **`prop.data`**: value is always set from JavaScript (objects, arrays, callbacks, class instances); the attribute is never read. Use this for both data and function props.\n\nWhen you need custom parsing or `reflect: false`, use a raw `PropDef` object:\n\n```ts\nprops: {\n items: { default: [], parse: () => [], reflect: false },\n}\n```\n\nUse `prop.data` for props that hold JS-only values (including callbacks) that cannot be serialised through an HTML attribute:\n\n```ts\ndefine('data-grid', {\n props: {\n getRowKey: prop.data<(row: unknown) => string>(),\n columns: prop.data<DataGridColumn[]>([]),\n onSort: prop.data<(key: string) => void>(),\n },\n setup(props) {\n // Set from JS: grid.getRowKey = (row) => row.id\n return html`...`;\n },\n});\n```\n\n## Template and Directives\n\n### `html`\n\nTagged template literal that returns an `HTMLResult`. Supports text interpolation, ordinary attributes (`attr=`),\nboolean attributes (`?attr=`), explicit property bindings (`.prop=`), events (`@event=` with optional\n`.stop`/`.prevent`/`.once`/`.capture` modifiers), refs (`ref=`), and nested templates.\n\n`.prop=${value}` always writes the element property (never an attribute): the explicit channel for structured\nvalues (arrays, objects, callbacks). Prototype-polluting names (`__proto__`, `constructor`, `prototype`) are\nrejected at compile time.\n\n`@event.stop`, `.prevent`, `.once`, and `.capture` are shorthand for the matching listener behavior\n(`stopPropagation`/`preventDefault` applied before the handler runs; `once`/`capture` passed as listener\noptions). An unknown modifier throws at compile time.\n\n### `css`\n\nTagged template literal that returns a `CSSResult` for use in `styles`.\n\n### Directives\n\n| Directive | Purpose |\n| -------------------------------------- | ----------------------------------------------------------------------------------------------------- |\n| `each(source, key, render, fallback?)` | Keyed reactive list; render receives `Readable<T>` and `Readable<number>`; plain `T[]` is a one-time static snapshot |\n| `when(condition, truthy, falsy?)` | Conditional rendering |\n| `classMap(record)` | Reactive class string from object map |\n| `styleMap(record)` | Reactive inline style string from object map |\n| `live(signal)` | One-way binding that skips stale writes during active user input; use with `@input` handler |\n| `model(signal, options?)` | Two-way form-control binding: writes the signal to `value`/`checked` and writes user input back on the control's change event |\n| `unsafeHtml(value)` | HTML rendering sink; sanitize untrusted values before calling |\n\n`each()` treats number and string keys as distinct. A reconciliation failure (a duplicate key, or a render function that throws) reports `ore:error` and preserves the last valid list: entries created during the failed pass are rolled back, so the previous list stays exactly on screen.\n\n### `model(signal, options?)`\n\nTwo-way binding for a form control: `value=${model(signal)}`. It writes the signal to the control's `value`/`checked` property on every change and writes user input back into the signal on the control's change event (`'change'` for checkboxes, radios, and `<select>`; `'input'` otherwise). This is the complete controlled-input loop that the `live()` + manual `@input` handler pair used to require.\n\n```ts\nconst text = signal('');\nconst agree = signal(false);\nconst count = signal(0);\n\nhtml`\n <input value=${model(text)} />\n <input type=\"checkbox\" checked=${model(agree)} />\n <input type=\"number\" value=${model(count, { get: (el) => Number(el.value) })} />\n`;\n```\n\n`options` overrides the defaults for non-native controls: `event` sets the write-back event name and `get` reads the current value out of the element (default `el.checked` for checkboxes/radios, `el.value` otherwise). Because a native text input's `value` is always a string, pass a `get` like `(el) => Number(el.value)` to keep a numeric signal numeric. `model()` can only drive a plain `value`/`checked` attribute binding; using it on a boolean (`?checked=`) or other attribute throws.\n\n### `unsafeHtml`\n\n`unsafeHtml()` is an explicit HTML injection sink. It has no global sanitizer: sanitize untrusted\ncontent before passing it to the directive, so the trust boundary remains at the call site.\n\n```ts\nimport { unsafeHtml } from '@vielzeug/ore';\n\nconst safeArticle = sanitize(userSuppliedArticle);\n\nreturn html`<article>${unsafeHtml(safeArticle)}</article>`;\n```\n\n## Host Bindings\n\n`bind(config, options?)` is a plain function imported from `@vielzeug/ore`:\n\n```ts\nbind({\n attr: { role: 'button', 'aria-expanded': () => String(open.value) },\n class: { 'is-open': open },\n style: { '--height': () => height.value + 'px' },\n on: { click: handleClick },\n});\n```\n\n`bind()` auto-registers cleanup with the component scope: no manual `onCleanup` needed. Returns a cleanup function for early teardown.\n\n### Off-host bindings\n\nPass `{ target: el }` as a second argument to bind to any element other than the host:\n\n```ts\nbind(\n { attr: { 'aria-expanded': () => String(isOpen.value) } },\n { target: triggerEl },\n);\n```\n\nEvent listener options (`once`, `capture`, `passive`) are also accepted in the second argument. Cleanup is auto-registered with the component scope when called during setup.\n\n### Reactive ARIA attributes\n\nUse explicit `aria-*` keys in `bind({ attr }, { target })`:\n\n```ts\nconst stopAria = bind(\n {\n attr: {\n 'aria-controls': panelId,\n 'aria-expanded': () => isOpen.value,\n 'aria-haspopup': 'listbox',\n },\n },\n { target: triggerEl },\n);\n```\n\nStatic values are applied once. Getters and signals create reactive effects. Calling `stopAria()` stops updates. Values of `null`, `undefined`, or `false` remove the attribute.\n\n## Slots\n\n- `slots.has(name?)`: `Readable<boolean>`: whether the named (or default) slot has assigned content\n- `slots.elements(name?)`: `Readable<Element[]>`: the assigned elements for the slot\n\nSlot signals update reactively when assigned content changes, including when slots are inserted dynamically (via `when()` or `each()`) after mount.\n\n## Context API\n\n- `createContext<T>(description)`: Create a unique typed injection key; the description is diagnostic only\n- `provide(key, value)`: Provide a value to descendants\n- `inject(key)`: Resolve from nearest ancestor; returns `undefined` if not found\n- `inject(key, fallback)`: Resolve with a fallback value\n- `injectStrict(key)`: Resolve or throw if absent\n\n`provide()` and `inject()` must be called synchronously during `setup()`. Calling them outside a setup context throws\n`'Lifecycle hooks must be called during component setup'`. Context resolution walks the ancestor chain including shadow\nDOM boundaries. `inject()` resolves and caches its result once per consumer: provide a `Readable` (signal/computed)\nrather than a raw value if descendants need to observe later changes; re-calling `provide()` with a new raw value\nafterward is not seen by consumers that already resolved it (a dev-mode warning fires when a key is provided twice on\nthe same element). `provide()` registers cleanup automatically: context keys are removed from the registry when the\nproviding component disconnects, so reconnecting the same element runs `setup()` fresh without spurious \"overwriting\"\nwarnings or stale keys leaking to descendants.\n\n## Utilities\n\n- `ref<T>()`: Create a `Signal<T | null>` element reference. Set to the element via `ref=` in templates.\n- `createId(prefix = 'id')`: Generate a collision-resistant ID with a semantic prefix, a per-runtime tag, and a monotonic counter (for example, `'field-a3k21'`).\n\n## Form-Associated API\n\nImport from `@vielzeug/ore`.\n\n### `useField(options)`\n\nWire a form-associated element to `ElementInternals`. Requires `formAssociated: true` on the component definition. The `disabled` state tracking via `internals.states` (CustomStateSet) is skipped with a dev warning if the API is unavailable in the current environment.\n\n```ts\ntype FormFieldValidity = { flags: ValidityStateFlags; message: string };\n\ntype FormFieldOptions<T> = {\n disabled?: Readable<boolean>;\n /**\n * When true, a null/undefined value is submitted as '' instead of null,\n * keeping the field's key present in FormData even when the value is absent.\n * Only applies to the default toFormValue; ignored if toFormValue is provided.\n * @default false\n */\n emptyStringForNull?: boolean;\n /** Called when the ancestor <form> resets (see onFormReset): restore local field state here. */\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n /**\n * Recomputed reactively and passed straight to internals.setValidity().\n * null (or omitted) = always valid. Flags and message travel together because\n * the platform's setValidity() contract couples them (flags without a\n * non-empty message throw); a single { flags, message } | null input makes a\n * desynced pair unrepresentable.\n */\n validity?: Readable<FormFieldValidity | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity(): boolean;\n readonly internals: ElementInternals;\n reportValidity(): boolean;\n /** Set (non-empty message) or clear (empty string) a custom validity error. */\n setCustomValidity(message: string): void;\n};\n```\n\nPass a single `validity` signal to make `required`-style constraints participate in native constraint validation\nthrough `checkValidity()` and `reportValidity()`. The `{ flags, message }` pair is the one validity input: the old\nsplit `validity` + `validationMessage` options are gone, since they could drift apart into a state the platform\nrejects.\n\n```ts\nconst isBlank = (v: string) => v.trim() === '';\n\nuseField({\n validity: computed(() =>\n required.value && isBlank(value.value)\n ? { flags: { valueMissing: true }, message: 'This field is required.' }\n : null,\n ),\n value,\n});\n```\n\n## Testing APIs\n\nImport from `@vielzeug/ore/testing`.\n\n| API | Purpose |\n| ------------------------ | ------------------------------------------------------------------------------------------ |\n| `mount(setup, options?)` | Mount a component and return a test fixture |\n| `cleanup()` | Remove all mounted elements and reset test state |\n| `install(afterEach)` | Register auto-cleanup after each test |\n| `flush(options?)` | Drain reactive updates and animation frames |\n| `renderHook(setup)` | Run lifecycle hooks in isolation; overload accepts `propDefs` as first arg for typed props |\n| `OreTimeoutError` | Error thrown when `flush()` cannot settle tracked Ore work |\n\n> **Test isolation:** `cleanup()` removes mounted elements and resets Ore's cross-test caches and counters. Call it in `afterEach`, or use `install(afterEach)`.\n\nImport `within`, named dispatchers such as `fireClick`, and waits such as `waitUntil` or `waitForEvent` from\n`@vielzeug/assay`.\n\n> **Form-associated component testing:** jsdom implements none of the `ElementInternals` form-association API.\n> `@vielzeug/assay` exports `installFormInternalsPolyfill()` for this: it polyfills\n> `setFormValue`/`setValidity`/`checkValidity`/`reportValidity`/`validationMessage`/`validity`/`states`, mixes\n> `checkValidity`/`reportValidity`/`validity`/`validationMessage` onto the host element itself (real browsers do\n> this for any `formAssociated: true` element), makes `FormData` collect a form-associated element's set value, and\n> makes `<form>.reset()` invoke `formResetCallback()`. Every patch is a guarded no-op when its target already\n> exists, and the function returns an `uninstall()` that restores every patched global. Call it directly from a\n> test setup (alongside `install(afterEach)`) only when the suite tests form-associated components: the patches\n> are global, so suites without them shouldn't carry them. A downstream package (e.g. a component library built\n> on `ore`) should rely on it instead of hand-rolling its own copy.\n\n#### `Fixture` interface\n\n```ts\ninterface Fixture<T extends HTMLElement = HTMLElement> {\n [Symbol.dispose](): void; // Delegates to dispose(): enables `using` declarations\n element: T;\n readonly disposed: boolean; // true after dispose() has been called\n readonly shadow: ShadowRoot | null;\n get<E extends Element>(selector: string): E;\n query<E extends Element>(selector: string): E | null;\n queryAll<E extends Element>(selector: string): E[];\n getByText<E extends Element>(text: string, selector?: string): E;\n queryByText<E extends Element>(text: string, selector?: string): E | null;\n queryAllByText<E extends Element>(text: string, selector?: string): E[];\n getByTestId<E extends Element>(testId: string): E;\n queryByTestId<E extends Element>(testId: string): E | null;\n queryAllByTestId<E extends Element>(testId: string): E[];\n attr(name: string, value: string | number | boolean): Promise<void>;\n attrs(record: Record<string, string | number | boolean>): Promise<void>;\n flush(options?: FlushOptions): Promise<void>;\n act(fn: () => unknown): Promise<void>;\n dispose(): void; // Removes the component from the DOM: idempotent\n}\n```\n\n#### `renderHook`\n\nUseful for testing composable lifecycle hooks (`onMounted`, `watchEffect`, `inject`, etc.) without a template. `onMounted`/`onCleanup`/`watchEffect`/... work exactly as inside a real `setup()`, since they resolve the same implicit current-component context:\n\n```ts\n// Without props\nconst { result, flush, dispose } = await renderHook(() => {\n const count = signal(0);\n onMounted(() => {\n count.value = 1;\n });\n return count;\n});\nexpect(result.value).toBe(1);\n\n// With typed props (prop-defs overload)\nconst { result } = await renderHook({ label: prop.string('hello'), count: prop.number(0) }, (props) => props.label);\nexpect(result.value).toBe('hello');\n```\n\n## Ripple Primitives\n\nOre does **not** re-export reactive primitives. Import them directly from `@vielzeug/ripple`:\n\n```ts\nimport { batch, computed, signal, watch } from '@vielzeug/ripple';\n```\n\nSee the [Ripple documentation](/ripple/) for the full API.\n\n## Lifecycle Events\n\n| Event | When |\n| ------------- | ------------------------------------------------------------------------- |\n| `ore:error` | When a lifecycle callback fails: bubbles, composed; detail is `OreLifecycleError` |\n\n## Types\n\n```ts\ntype PropDef<T> = {\n readonly default: T;\n readonly parse: (value: string | null) => T;\n reflect?: boolean;\n};\n\ntype PropsDef<T extends Record<string, unknown>> = {\n [K in keyof Required<T>]: PropDef<T[K & keyof T]>;\n};\n\ntype PropInputDefs = Record<string, PropDef<unknown>>;\n\n/**\n * Infer reactive props type from a PropInputDefs map.\n * Each entry becomes Readable<T> keyed by prop name.\n */\ntype InferProps<D extends PropInputDefs> = {\n readonly [K in keyof D]-?: Readable<InferPropValue<D[K]>>;\n};\n\n// Runtime hooks: all plain functions imported from '@vielzeug/ore', not fields on an object.\ntype OnMountedCallback = () => Cleanup | undefined;\ntype OnFormResetCallback = () => void;\n\ndeclare function onMounted(fn: OnMountedCallback): void; // DOM-ready callback; runs after each connection's render\ndeclare function onCleanup(fn: Cleanup): void; // Register teardown; called on disconnect\ndeclare function onElement<T extends HTMLElement>(\n ref: Readable<T | null>,\n callback: (el: T) => Cleanup | undefined,\n): () => void;\ndeclare function onEvent<K extends keyof HTMLElementEventMap>(\n target: EventTarget | null | undefined,\n event: K,\n listener: (e: HTMLElementEventMap[K]) => void,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onEvent(\n target: EventTarget | null | undefined,\n event: string,\n listener: EventListener,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onFormReset(fn: OnFormResetCallback): void; // Runs on every ancestor <form> reset; formAssociated only\ndeclare function watchEffect(fn: () => Cleanup | undefined): () => void; // Scoped reactive effect; auto-cleaned on disconnect\ndeclare function bind(config: HostBindConfig, options?: BindOptions): () => void; // Bindings for host or any target element\ndeclare function provide<T>(key: InjectionKey<T>, value: T): void; // Register a context value on the host element\ndeclare function inject<T>(key: InjectionKey<T>): T | undefined;\ndeclare function inject<T>(key: InjectionKey<T>, fallback: T): T;\ndeclare function getHost(): HTMLElement; // The current component's host element\ndeclare function useEmit<Emits extends Record<string, unknown> = Record<string, never>>(): EmitFn<Emits>;\ndeclare function useSlots<SlotNames extends string = string>(): ComponentSlots<SlotNames>;\n\ntype ComponentDefinition<Props extends Record<string, unknown> = Record<never, never>> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n\ntype MaybeReactive<T> = T | Readable<T> | (() => T); // Plain value, signal, or getter: accepted by when(), each(), classMap(), and bind()\n\ntype HostBindingValue = MaybeReactive<string | number | boolean | null | undefined>;\n\ntype AttributeBindings = Record<string, HostBindingValue>;\n\ntype HostBindConfig = {\n attr?: AttributeBindings;\n class?: (() => Record<string, boolean>) | Record<string, Readable<boolean> | (() => boolean) | boolean>;\n on?: Record<string, ((event: Event) => void) | undefined>;\n style?: Record<string, HostBindingValue>;\n};\n\ntype BindOptions = AddEventListenerOptions & {\n target?: Element;\n};\n\ntype ComponentSlots<S extends string = string> = {\n elements(name?: S): Readable<Element[]>;\n has(name?: S): Readable<boolean>;\n};\n\ntype Ref<T extends Element> = Signal<T | null>;\n\ntype RefCallback<T extends Element> = (el: T | null) => void;\n\ntype InjectionKey<T> = symbol & { readonly __ore_injection_key?: T };\n\ninterface HTMLResult {\n mount(\n parent: ParentNode,\n anchor: Node | null,\n registerCleanup: (fn: () => void) => void,\n ): Node[];\n}\n\ntype CSSResult = {\n content: string;\n toString(): string;\n};\n\ntype LiveBinding<T> = { readonly source: Readable<T> };\n\ntype ModelOptions<T = unknown> = {\n /** Write-back event (default: 'change' for checkbox/radio/select, 'input' otherwise). */\n event?: string;\n /** Value reader (default: el.checked for checkbox/radio, el.value otherwise). */\n get?: (el: HTMLElement) => T;\n};\n\ntype EmitFn<T extends Record<string, unknown>> = {\n <K extends KeysWithoutDetail<T>>(event: K): boolean;\n <K extends Exclude<keyof T, KeysWithoutDetail<T>>>(event: K, detail: T[K]): boolean;\n};\n// KeysWithoutDetail is an internal helper type, not exported.\n\ntype FormFieldValidity = { flags: ValidityStateFlags; message: string };\n\ntype FormFieldOptions<T = unknown> = {\n disabled?: Readable<boolean>;\n emptyStringForNull?: boolean;\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n validity?: Readable<FormFieldValidity | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity: () => boolean;\n readonly internals: ElementInternals;\n reportValidity: () => boolean;\n setCustomValidity: (message: string) => void;\n};\n\n/** Phase in which a OreError occurred. */\ntype OreErrorPhase = 'each-reconcile' | 'form-reset' | 'mounted' | 'setup';\n```\n\n## Errors\n\n`OreError` is the base class for every Ore error class: `err instanceof OreError` catches all of them.\n\n- **`OreApiError`**: thrown when the `ore` API itself is misused: calling `define()` with a duplicate tag, calling a lifecycle hook (`inject`, `onMounted`, `onCleanup`, `onEvent`, …) outside of `setup()`, or passing an invalid prop definition to `define()`.\n- **`OreInternalError`**: thrown when an Ore invariant fails, indicating a package bug rather than invalid application code.\n- **`OreLifecycleError`**: reported in the `ore:error` event when component `setup()`, a mounted callback, a form-reset callback, or `each()` reconciliation fails. Extends `OreError` with:\n - `component: string`: the element's local name\n - `phase: OreErrorPhase`: `'setup'` | `'mounted'` | `'form-reset'` | `'each-reconcile'`\n - `cause: Error`: the original error thrown by `setup()`\n- **`OreTimeoutError`**: thrown by `flush()` (from `@vielzeug/ore/testing`) when pending Ore work does not settle before its timeout.\n\nLifecycle failures dispatch a bubbling, composed `ore:error` event whose `detail` is the `OreLifecycleError`. Setup\nfailures still rethrow their original error; mounted and form-reset callback failures are reported through the same\nevent so their remaining callbacks can continue.\n",
|
|
6
|
+
"usage": "---\ntitle: 'Ore: Usage Guide'\ndescription: Practical Ore usage patterns for components, props, templates, slots, context, forms, Sentinel integration, and tests.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`define(tag, definition)` creates and registers a custom element. Call it from the module or browser bootstrap responsible for registration.\n\nYour `setup()` function receives typed prop signals and returns an `HTMLResult` directly. Its state belongs to the\ncurrent connection: disconnect disposes it, and reconnecting the same element runs setup again.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('status-chip', {\n setup() {\n const online = signal(true);\n\n return html`\n <button @click=${() => (online.value = !online.value)}>${() => (online.value ? 'Online' : 'Offline')}</button>\n `;\n },\n});\n```\n\nEverything besides `props`: lifecycle hooks, host bindings, context, slots, emit: is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, getHost, html, bind, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-widget', {\n setup(_props) {\n const el = getHost(); // the host HTMLElement\n const emit = useEmit<{ close: undefined }>(); // typed event emitter\n const slots = useSlots<'header'>(); // reactive slot observation\n\n bind({ attr: { role: 'group' } }); // host binding helper (attr, class, style, on)\n\n return html`<slot></slot>`;\n },\n});\n```\n\n## signals and effects\n\nOre does not re-export ripple primitives: import them directly from `@vielzeug/ripple`.\n\n```ts\nimport { batch, computed, effect, signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst doubled = computed(() => count.value * 2);\n\neffect(() => {\n console.log('doubled =', doubled.value);\n});\n\nwatch(count, (next, prev) => {\n console.log('count changed', prev, '->', next);\n});\n\nbatch(() => {\n count.value = 1;\n count.value = 2;\n});\n```\n\n## onMounted and lifecycle\n\nUse `onMounted()` for DOM-dependent initialization that must run after the template is mounted. Use `onElement(ref, cb)` for work tied to a specific DOM node. `onEvent()` attaches a listener that is automatically removed on disconnect.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, onElement, onEvent, onMounted, ref, useSlots } from '@vielzeug/ore';\n\ndefine('deferred-init', {\n setup(_props) {\n const tabIndex = signal(0);\n const inputRef = ref<HTMLInputElement>();\n const slots = useSlots<'items'>();\n\n onMounted(() => {\n const items = slots.elements('items').value;\n console.log('Found', items.length, 'items');\n });\n\n onElement(inputRef, (input) => {\n input.focus();\n });\n\n onEvent(window, 'keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') tabIndex.value = 0;\n });\n\n return html`<div><slot name=\"items\"></slot><input ref=${inputRef} /></div>`;\n },\n});\n```\n\n## prop definitions\n\nUse `prop.*` helpers for common cases, or raw `PropDef` objects for custom parsing or `reflect: false`.\n\n```ts\nimport { define, html, prop } from '@vielzeug/ore';\n\ndefine('x-button', {\n props: {\n label: prop.string('Button'),\n disabled: prop.bool(false),\n variant: prop.oneOf(['primary', 'secondary'] as const, 'primary'),\n count: prop.number(0),\n },\n setup(props) {\n return html`\n <button ?disabled=${props.disabled} data-variant=${props.variant}>${props.label} (${props.count})</button>\n `;\n },\n});\n```\n\n## template bindings\n\n`html` supports text, attributes, booleans, properties, events, refs, and nested templates. Event bindings accept\n`.stop`, `.prevent`, `.once`, and `.capture`: the first two apply before the handler runs, while the latter two use\nnative listener options. Unknown modifiers throw while Ore compiles the template.\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { define, html, model, ref } from '@vielzeug/ore';\n\ndefine('profile-name', {\n setup() {\n const name = signal('Alice');\n const inputRef = ref<HTMLInputElement>();\n\n return html`\n <label title=${computed(() => 'Current: ' + name.value)}>Name</label>\n <input\n ref=${inputRef}\n value=${model(name)}\n aria-label=${() => 'Current name ' + name.value} />\n <p>Hello ${name}</p>\n `;\n },\n});\n```\n\n## directives\n\nOre exports `each`, `classMap`, `styleMap`, `when`, `live`, `model`, and `unsafeHtml` from `@vielzeug/ore`.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { classMap, define, each, html, styleMap, when } from '@vielzeug/ore';\n\ndefine('task-list', {\n setup() {\n const tasks = signal([{ id: 1, text: 'Write tests' }]);\n const active = signal(true);\n\n return html`\n <ul\n class=\"${classMap({ ready: () => tasks.value.length > 0 })}\"\n style=${styleMap({ opacity: () => (active.value ? 1 : 0.5) })}>\n ${when(\n () => active.value,\n () => html`<li>Active</li>`,\n () => html`<li>Paused</li>`,\n )}\n ${each(\n tasks,\n (task) => task.id,\n (task) => html`<li>${() => task.value.text}</li>`,\n )}\n </ul>\n `;\n },\n});\n```\n\n### each() API\n\n`each(source, key, render, fallback?)` takes positional arguments:\n\n- **source**: signal, getter, or plain array\n- **key**: function returning a unique string or number per item; number and string keys remain distinct\n- **render**: receives reactive `item` and `index` signals\n- **fallback**: optional, rendered when the list is empty\n\n```ts\neach(\n items,\n (item) => item.id,\n (item, index) => html`<li>#${index}: ${() => item.value.label}</li>`,\n () => html`<li>No items</li>`,\n);\n```\n\nEach reactive reconciliation is transactional. Ore creates missing entries before committing updates or removing stale\nentries; if a render callback throws, entries created during that pass are rolled back and the last successful list\nremains on screen. Ore reports the failure through `ore:error` with phase `each-reconcile`.\n\n## model: two-way form bindings\n\nUse `model(signal)` for a controlled input: it writes the signal to the control's `value`/`checked` property and\nwrites user input back on the control's change event (`'change'` for checkboxes, radios, and selects; `'input'`\notherwise). This replaces the `live()` + manual `@input` handler pair with one binding.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, model } from '@vielzeug/ore';\n\ndefine('live-search', {\n setup() {\n const query = signal('');\n const agree = signal(false);\n\n return html`\n <input value=${model(query)} />\n <input type=\"checkbox\" checked=${model(agree)} />\n `;\n },\n});\n```\n\nA native text input's `value` is always a string, so pass a `get` reader to keep a numeric signal numeric, and an\n`event` name for custom controls that emit their own change event:\n\n```ts\nconst count = signal(0);\n\nhtml`\n <input type=\"number\" value=${model(count, { get: (el) => Number(el.value) })} />\n <my-picker value=${model(pick, { event: 'pick-change', get: (el) => (el as MyPicker).selection })} />\n`;\n```\n\n## live: one-way bindings that preserve user edits\n\nUse `live(signal)` when a binding must skip a stale app-state write while the user is mid-edit, without writing\nuser input back. `model()` covers the common controlled-input case; `live()` is for one-way bindings that only\nneed the stale-write guard.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, live } from '@vielzeug/ore';\n\ndefine('live-search', {\n setup() {\n const query = signal('');\n\n return html`\n <input value=${live(query)} @input=${(e: Event) => (query.value = (e.target as HTMLInputElement).value)} />\n `;\n },\n});\n```\n\n## host bindings\n\n`bind()` wires reactive attrs, classes, styles, and events to the host element.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html } from '@vielzeug/ore';\n\ndefine('x-toggle', {\n setup(_props) {\n const open = signal(false);\n\n bind({\n attr: { 'aria-expanded': () => String(open.value), role: 'button', tabindex: 0 },\n class: { 'is-open': open },\n on: { click: () => (open.value = !open.value) },\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nThe `bind` config supports `attr`, `class`, `style`, and `on` sections.\n\n## ARIA bindings\n\nUse explicit `aria-*` keys in `bind({ attr: config }, { target })` to reactively sync ARIA attributes to any element.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onMounted } from '@vielzeug/ore';\n\ndefine('x-disclosure', {\n setup(_props) {\n const open = signal(false);\n const panelId = 'disclosure-panel';\n\n bind({\n attr: { role: 'button', tabindex: 0 },\n on: { click: () => (open.value = !open.value) },\n });\n\n onMounted(() => {\n const trigger = document.querySelector('#trigger') as HTMLElement;\n if (trigger) {\n // bind() registers cleanup automatically when called inside setup\n bind(\n {\n attr: {\n 'aria-controls': panelId,\n 'aria-expanded': () => String(open.value),\n 'aria-haspopup': 'region',\n },\n },\n { target: trigger },\n );\n }\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nStatic values are applied once. Getter functions create reactive effects. Setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n`bind()` always returns a cleanup function. Use it to stop syncing early when a trigger element can be swapped out:\n\n```ts\nonMounted(() => {\n const trigger = document.querySelector('#trigger') as HTMLElement;\n const stopAria = bind({ attr: { 'aria-expanded': () => String(open.value) } }, { target: trigger });\n\n // Stop syncing when the trigger is replaced\n onCleanup(stopAria);\n});\n```\n\n### Binding a non-host element with `bind()`\n\nPass `{ target: el }` as a second argument to bind attributes, classes, styles, or events to any element:\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onMounted, ref } from '@vielzeug/ore';\n\ndefine('button-wrapper', {\n setup(_props) {\n const visible = signal(false);\n const btnRef = ref<HTMLButtonElement>();\n\n onMounted(() => {\n const btn = btnRef.value;\n if (!btn) return;\n\n bind(\n {\n attr: { 'aria-pressed': () => String(visible.value) },\n on: { click: () => (visible.value = !visible.value) },\n },\n { target: btn },\n );\n });\n\n return html`<button ref=${btnRef}>Toggle</button>`;\n },\n});\n```\n\n## slots and emits\n\n```ts\nimport { define, html, useEmit, useSlots, when } from '@vielzeug/ore';\n\ndefine('card-with-footer', {\n setup(_props) {\n const slots = useSlots<'header' | 'footer'>();\n const emit = useEmit<{ action: undefined }>();\n\n return html`\n <div class=\"card\">\n <slot name=\"header\"></slot>\n <slot></slot>\n ${when(slots.has('footer'), () => html`<footer><slot name=\"footer\"></slot></footer>`)}\n </div>\n <button @click=${() => emit('action')}>Go</button>\n `;\n },\n});\n```\n\nPass a `SlotNames` type parameter to `useSlots<SlotNames>()` to get typed `slots.has()` and `slots.elements()` calls.\n\n## context provide/inject\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { createContext, define, html, injectStrict, provide } from '@vielzeug/ore';\n\nconst COUNT_CTX = createContext<ReturnType<typeof signal<number>>>('count');\n\ndefine('count-provider', {\n setup(_props) {\n const count = signal(0);\n provide(COUNT_CTX, count);\n\n return html`<button @click=${() => count.value++}><slot></slot></button>`;\n },\n});\n\ndefine('count-consumer', {\n setup() {\n const count = injectStrict(COUNT_CTX);\n\n return html`<p>Count: ${count}</p>`;\n },\n});\n```\n\n`provide()` registers cleanup automatically: context keys are removed from the registry when the providing component disconnects. On reconnect, `setup()` runs fresh and `provide()` re-registers without spurious \"overwriting\" warnings. Provide a `Readable` (signal/computed) rather than a raw value if descendants need to observe later changes: `inject()` resolves and caches the value once per consumer connection.\n\n## form-associated elements\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, prop } from '@vielzeug/ore';\nimport { useField } from '@vielzeug/ore';\n\ndefine('rating-input', {\n formAssociated: true,\n setup() {\n const value = signal(0);\n const field = useField({ value });\n\n return html`\n <button @click=${() => (value.value = 1)}>1</button>\n <button @click=${() => (value.value = 2)}>2</button>\n <button @click=${() => (value.value = 3)}>3</button>\n <button @click=${() => field.reportValidity()}>Validate</button>\n <p>Current: ${value}</p>\n `;\n },\n});\n```\n\n## Sentinel Observers\n\nUse `@vielzeug/sentinel` for subscribable browser and DOM observations. Create element-dependent Sentinels inside `onMounted()` and dispose them with the component.\n\n```ts\nimport { define, html, onCleanup, onMounted, ref, watchEffect } from '@vielzeug/ore';\nimport { fromSubscribable } from '@vielzeug/ripple';\nimport { createElementSize, SentinelUnavailableError } from '@vielzeug/sentinel';\n\ndefine('x-observed', {\n setup(_props) {\n const boxRef = ref<HTMLDivElement>();\n\n onMounted(() => {\n const element = boxRef.value;\n if (!element) return;\n\n try {\n const size = createElementSize(element);\n const sizeState = fromSubscribable(size, { signal: size.disposalSignal });\n\n watchEffect(() => {\n console.log(sizeState.value?.width);\n });\n\n onCleanup(() => size.dispose());\n } catch (error) {\n if (!(error instanceof SentinelUnavailableError)) throw error;\n }\n });\n\n return html`<div ref=${boxRef}>Observe me</div>`;\n },\n});\n```\n\n## testing utilities\n\nImport from `@vielzeug/ore/testing`.\n\n```ts\nimport { afterEach, describe, expect, it } from 'vitest';\nimport { signal } from '@vielzeug/ripple';\nimport { fireClick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { cleanup, mount } from '@vielzeug/ore/testing';\n\ndescribe('my-counter', () => {\n afterEach(cleanup);\n\n it('increments on click', async () => {\n let count!: ReturnType<typeof signal<number>>;\n const { query, act } = await mount(() => {\n count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n });\n\n expect(query('button')?.textContent).toBe('0');\n\n await act(() => fireClick(query('button')!));\n\n expect(query('button')?.textContent).toBe('1');\n });\n});\n```\n\n## Framework Integration\n\nOre components are standard custom elements and work natively in any framework.\n\n::: code-group\n\n```tsx [React]\n// React 19+ supports custom elements natively.\nimport './x-toggle'; // wherever define('x-toggle', { ... }) is called\n\nfunction App() {\n return <x-toggle aria-label=\"Open menu\" />;\n}\n```\n\n```ts [Vue 3]\n<script setup lang=\"ts\">\nimport './x-toggle'; // wherever define('x-toggle', { ... }) is called\nimport { ref } from 'vue';\n\nconst open = ref(false);\n</script>\n\n<template>\n <x-toggle :aria-label=\"'Open menu'\" @click=\"open = !open\" />\n</template>\n```\n\n```svelte [Svelte]\n<script>\n import './x-toggle'; // wherever define('x-toggle', { ... }) is called\n\n function handleClick() {\n console.log('toggled');\n }\n</script>\n\n<x-toggle aria-label=\"Open menu\" on:click={handleClick} />\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Ripple\n\nImport ripple primitives directly from `@vielzeug/ripple` for standalone reactive state outside components.\n\n```ts\nimport { signal, computed } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\n// Shared state created outside any component\nconst theme = signal<'light' | 'dark'>('light');\nconst isDark = computed(() => theme.value === 'dark');\n\ndefine('theme-toggle', {\n setup() {\n return html`\n <button @click=${() => (theme.value = isDark.value ? 'light' : 'dark')}>\n ${() =>\n isDark.value ? '<ore-icon name=\"sun\" size=\"16\"></ore-icon>' : '<ore-icon name=\"moon\" size=\"16\"></ore-icon>'}\n </button>\n `;\n },\n});\n```\n\n### With Forge\n\nUse `@vielzeug/forge` for typed form state. `useField()` remains intentionally narrow: it connects a form-associated\ncustom element to native `ElementInternals` without imposing submission, validation, or dirty-state policy.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('signup-form', {\n setup(_props) {\n const form = createForm({ initialValues: { email: '' } });\n\n return html`\n <form\n @submit=${(event: SubmitEvent) => {\n event.preventDefault();\n void form.submit(async (values) => {\n console.log(values);\n });\n }}>\n <slot></slot>\n </form>\n `;\n },\n});\n```\n\n## Best Practices\n\n- Setup returns `html\\`...\\`` directly: not a function wrapping the template.\n- Use `watchEffect()` for reactive subscriptions tied to component lifetime: it auto-registers cleanup on disconnect.\n- Use `onElement(ref, cb)` instead of `onMounted` when the work is tied to a single DOM node.\n- Bind host attributes and classes via `bind()` rather than mutating the element directly.\n- Provide context at the nearest ancestor: avoid global context singletons.\n- Call `onCleanup()` for every resource allocated in `setup()` (WebSockets, intervals, external subscriptions).\n- Use `model(signal)` for two-way form inputs; use `live(signal)` for a one-way binding that must not clobber user-in-progress edits.\n- Extract composable helper functions freely: `onMounted`/`onCleanup`/`bind`/... resolve the active component through implicit context, so they work from any function called (transitively) during `setup()`, with no need to pass them in as parameters.\n- Test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic DOM events, queries, and waits\n from `@vielzeug/assay`.\n",
|
|
7
7
|
"examples": "---\ntitle: 'Ore: Examples'\ndescription: Practical examples and recipes for ore.\n---\n\n## Examples\n\n- [Counter Component](./examples/counter-component.md)\n- [Typed Props And Emits](./examples/typed-props-and-emits.md)\n- [Observers In onMounted()](./examples/observers-in-onmount.md)\n- [Search List With Directives](./examples/search-list-with-directives.md)\n- [Context Provider And Consumer](./examples/context-provider-and-consumer.md)\n- [Prop Helpers And Raw PropDef](./examples/propsof-builder-api.md)\n- [Form Associated Rating Input](./examples/form-associated-rating-input.md)\n- [Test Example With @vielzeug/ore/testing](./examples/test-example-at-vielzeug-ore-testing.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [],
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"each": "export { each } from './directives/each';",
|
|
20
20
|
"LiveBinding": "export { type LiveBinding, live } from './directives/live';",
|
|
21
21
|
"live": "export { type LiveBinding, live } from './directives/live';",
|
|
22
|
+
"ModelOptions": "export { type ModelOptions, model } from './directives/model';",
|
|
23
|
+
"model": "export { type ModelOptions, model } from './directives/model';",
|
|
22
24
|
"styleMap": "export { styleMap } from './directives/styleMap';",
|
|
23
25
|
"unsafeHtml": "export { unsafeHtml } from './directives/unsafe-html';",
|
|
24
26
|
"when": "export { when } from './directives/when';",
|
|
@@ -27,9 +29,10 @@
|
|
|
27
29
|
"OreErrorPhase": "export { OreApiError, OreError, type OreErrorPhase, OreInternalError, OreLifecycleError } from './errors';",
|
|
28
30
|
"OreInternalError": "export { OreApiError, OreError, type OreErrorPhase, OreInternalError, OreLifecycleError } from './errors';",
|
|
29
31
|
"OreLifecycleError": "export { OreApiError, OreError, type OreErrorPhase, OreInternalError, OreLifecycleError } from './errors';",
|
|
30
|
-
"FormFieldHandle": "export { type FormFieldHandle, type FormFieldOptions, useField } from './forms/field';",
|
|
31
|
-
"FormFieldOptions": "export { type FormFieldHandle, type FormFieldOptions, useField } from './forms/field';",
|
|
32
|
-
"
|
|
32
|
+
"FormFieldHandle": "export { type FormFieldHandle, type FormFieldOptions, type FormFieldValidity, useField } from './forms/field';",
|
|
33
|
+
"FormFieldOptions": "export { type FormFieldHandle, type FormFieldOptions, type FormFieldValidity, useField } from './forms/field';",
|
|
34
|
+
"FormFieldValidity": "export { type FormFieldHandle, type FormFieldOptions, type FormFieldValidity, useField } from './forms/field';",
|
|
35
|
+
"useField": "export { type FormFieldHandle, type FormFieldOptions, type FormFieldValidity, useField } from './forms/field';",
|
|
33
36
|
"AttributeBindings": "export {\n type AttributeBindings,\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindingValue,\n} from './host-bind';",
|
|
34
37
|
"BindOptions": "export {\n type AttributeBindings,\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindingValue,\n} from './host-bind';",
|
|
35
38
|
"bind": "export {\n type AttributeBindings,\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindingValue,\n} from './host-bind';",
|