@vielzeug/codex 2.2.7 → 2.2.8
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 +1693 -0
- package/data/llms-full.txt +27748 -0
- package/data/llms.txt +40 -0
- package/data/manifest.json +8 -0
- package/data/packages/arsenal.json +210 -0
- package/data/packages/assay.json +39 -0
- package/data/packages/clockwork.json +67 -0
- package/data/packages/codex.json +43 -0
- package/data/packages/coins.json +102 -0
- package/data/packages/conduit.json +60 -0
- package/data/packages/courier.json +58 -0
- package/data/packages/dnd.json +77 -0
- package/data/packages/familiar.json +40 -0
- package/data/packages/flux.json +93 -0
- package/data/packages/forge.json +84 -0
- package/data/packages/herald.json +108 -0
- package/data/packages/keymap.json +60 -0
- package/data/packages/ledger.json +57 -0
- package/data/packages/lingua.json +67 -0
- package/data/packages/necromancer.json +50 -0
- package/data/packages/orbit.json +99 -0
- package/data/packages/ore.json +73 -0
- package/data/packages/prism.json +66 -0
- package/data/packages/pulse.json +69 -0
- package/data/packages/refine.json +12 -0
- package/data/packages/ripple.json +83 -0
- package/data/packages/rune.json +79 -0
- package/data/packages/sandbox.json +40 -0
- package/data/packages/scout.json +60 -0
- package/data/packages/scroll.json +109 -0
- package/data/packages/sourcerer.json +72 -0
- package/data/packages/spell.json +133 -0
- package/data/packages/tempo.json +81 -0
- package/data/packages/vault.json +85 -0
- package/data/packages/ward.json +114 -0
- package/data/packages/wayfinder.json +110 -0
- package/data/refine.json +11926 -0
- package/data/search.json +1432 -0
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "export { createCursorSource } from './cursorSource';\nexport { createInfiniteSource } from './infiniteSource';\nexport { createLocalSource } from './localSource';\nexport { createPageSource } from './pageSource';\nexport type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';\n",
|
|
3
|
+
"docs": {
|
|
4
|
+
"index": "---\ntitle: Sourcerer — Reactive Query Sources\ndescription: Framework-agnostic collection sources for local, page, cursor, and infinite pagination.\npackage: sourcerer\ncategory: data\nkeywords: [pagination, data-source, cursor, infinite-scroll, search]\nrelated: [courier, ripple, scout, wayfinder]\nexports:\n [\n createCursorSource,\n createInfiniteSource,\n createLocalSource,\n createPageSource,\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n LoadContext,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"sourcerer\" />\n\n## Why Sourcerer?\n\nLists often combine pagination, search, request cancellation, and render state. Sourcerer gives local arrays and remote loaders one snapshot contract while leaving caching, retries, and transport policy to your application.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\ntype User = { id: number; name: string };\n\n// Before: query changes can mix old items with new loading and page state.\nlet items: User[] = [];\nlet page = 1;\nlet isLoading = false;\n\n// After: one source publishes internally consistent loaded state.\nconst source = createPageSource<User>({\n autoStart: false,\n load: async () => ({ data: [{ id: 1, name: 'Ada' }], total: 1 }),\n});\nsource.subscribe((snapshot) => console.log(snapshot.data));\nsource.dispose();\n```\n\n| Feature | Sourcerer | Manual list state | Courier query cache |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"sourcerer\" type=\"size\" /> | Application-defined | <PackageInfo package=\"courier\" type=\"size\" /> |\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| Local and remote collections | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Cursor and infinite pagination | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Latest-request cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | Transport-level |\n\n<div class=\"decision-callout\">\n\n**Use Sourcerer when** one UI collection needs local or remote pagination with an explicit, framework-independent snapshot contract.\n\n**Consider Courier alone when** you only need cached HTTP queries and pagination state belongs elsewhere.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/sourcerer\n```\n\n```sh [npm]\nnpm install @vielzeug/sourcerer\n```\n\n```sh [yarn]\nyarn add @vielzeug/sourcerer\n```\n\n:::\n\n## Quick Start\n\nCreate a page source, load it, then dispose it with its owner.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\ntype User = { id: number; name: string };\n\nconst source = createPageSource<User>({\n autoStart: false,\n load: async ({ query }) => {\n const users = [\n { id: 1, name: 'Ada' },\n { id: 2, name: 'Grace' },\n { id: 3, name: 'Linus' },\n ];\n const start = (query.page - 1) * query.pageSize;\n\n return { data: users.slice(start, start + query.pageSize), total: users.length };\n },\n});\n\ntry {\n await source.reload();\n console.log(source.snapshot.data);\n} catch (error) {\n console.error(error);\n} finally {\n source.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createLocalSource()` — synchronous search and numbered pagination over an array\n- `createPageSource()` — numbered remote pages with latest-request cancellation\n- `createCursorSource()` — sequential opaque-cursor navigation\n- `createInfiniteSource()` — append-only page loading\n- `SourceSnapshot` — loaded `query`, `data`, and `pagination` plus optional `pendingQuery`\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- [Courier](/courier/) — use as transport, caching, and retry policy inside a page loader\n- [Scout](/scout/) — adapt an indexed search matcher for local sources\n- [Ripple](/ripple/) — project source snapshots into reactive application state\n- [Wayfinder](/wayfinder/) — validate and synchronize page query fields with route state\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Sourcerer — API Reference\ndescription: Public API for @vielzeug/sourcerer.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution | Common gotcha |\n| --- | --- | --- | --- |\n| `createLocalSource()` | In-memory search and pagination | Sync | Prepare filtering and ranking before `setData()` |\n| `createPageSource()` | Numbered async pages | Async | `query` remains loaded state while `pendingQuery` is active |\n| `createCursorSource()` | Cursor-based async pages | Async | `after` and `before` cannot coexist |\n| `createInfiniteSource()` | Appended async pages | Async | `loadMore()` does nothing while fetching or exhausted |\n| `SourceSnapshot` | Atomic loaded state plus pending request | Type | Read `pendingQuery` for newer in-flight state |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/sourcerer` | Factories and public types |\n\n## Factories\n\n### `createLocalSource()`\n\n```ts\nfunction createLocalSource<T>(data: readonly T[], config?: LocalSourceConfig<T>): LocalSource<T>\n```\n\nCreates a synchronous source over an in-memory collection.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `initialQuery` | `LocalQueryPatch` | Initial page, page size, or search value |\n| `match` | `(item, search) => boolean` | Explicit search predicate |\n\n**Returns:** `LocalSource<T>`.\n\n```ts\nimport { createLocalSource } from '@vielzeug/sourcerer';\n\nconst users = createLocalSource(\n [{ id: 1, name: 'Ada' }],\n {\n initialQuery: { pageSize: 20 },\n match: (user, search) => user.name.toLowerCase().includes(search.toLowerCase()),\n },\n);\n\nusers.setQuery({ search: 'ada' });\n```\n\n---\n\n### `createPageSource()`\n\n```ts\nfunction createPageSource<T, TFilter = unknown, TSort = unknown>(\n config: PageSourceConfig<T, TFilter, TSort>,\n): PageSource<T, TFilter, TSort>\n```\n\nCreates a numbered source. New queries abort older work. Loaded state stays in `snapshot`; newer work appears in `snapshot.pendingQuery`.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `autoStart` | `boolean` | Start initial request; default `true` |\n| `initialQuery` | `PageQueryPatch<TFilter, TSort>` | Initial query values |\n| `load` | `(context) => Promise<PageResult<T>>` | Transport callback |\n\n**Returns:** `PageSource<T, TFilter, TSort>`.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst users = createPageSource({\n autoStart: false,\n load: async () => ({ data: [{ id: 1, name: 'Ada' }], total: 1 }),\n});\n\nawait users.setQuery({ page: 1 });\nusers.dispose();\n```\n\n---\n\n### `createCursorSource()`\n\n```ts\nfunction createCursorSource<T, TCursor = string>(\n config: CursorSourceConfig<T, TCursor>,\n): CursorSource<T, TCursor>\n```\n\nCreates a sequential cursor source. Search and page-size changes reset cursors.\n\n**Returns:** `CursorSource<T, TCursor>`.\n\n```ts\nimport { createCursorSource } from '@vielzeug/sourcerer';\n\nconst orders = createCursorSource({\n autoStart: false,\n load: async () => ({ data: ['order-1'] }),\n});\n\nawait orders.reload();\nawait orders.page.next();\norders.dispose();\n```\n\n---\n\n### `createInfiniteSource()`\n\n```ts\nfunction createInfiniteSource<T>(config: InfiniteSourceConfig<T>): InfiniteSource<T>\n```\n\nCreates an append-only source. Query changes replace loaded collection after successful first-page load.\n\n**Returns:** `InfiniteSource<T>`.\n\n```ts\nimport { createInfiniteSource } from '@vielzeug/sourcerer';\n\nconst feed = createInfiniteSource({\n autoStart: false,\n load: async () => ({ data: ['post-1'], total: 1 }),\n});\n\nawait feed.loadMore();\nfeed.dispose();\n```\n\n## Types\n\n### Source primitives\n\n```ts\ntype Disposable = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n};\n\ntype SourceSnapshot<T, TQuery, TPagination extends AnyPagination = AnyPagination> = Readonly<{\n data: readonly T[];\n error: Error | null;\n isFetching: boolean;\n pagination: TPagination;\n pendingQuery?: TQuery;\n query: TQuery;\n}>;\n\ntype Source<T, TQuery, TPagination extends AnyPagination = AnyPagination> = Disposable & {\n readonly snapshot: SourceSnapshot<T, TQuery, TPagination>;\n subscribe(listener: (snapshot: SourceSnapshot<T, TQuery, TPagination>) => void): () => void;\n};\n```\n\n### Numbered pages\n\n```ts\ntype PagePagination = Readonly<{\n count: number;\n hasNext: boolean;\n hasPrevious: boolean;\n index: number;\n kind: 'page';\n size: number;\n total: number;\n}>;\n\ntype PageQuery<TFilter = unknown, TSort = unknown> = Readonly<{\n filter?: TFilter;\n page: number;\n pageSize: number;\n search: string;\n sort?: TSort;\n}>;\n\ntype PageQueryPatch<TFilter = unknown, TSort = unknown> = Readonly<{\n filter?: TFilter | undefined;\n page?: number;\n pageSize?: number;\n search?: string;\n sort?: TSort | undefined;\n}>;\n\ntype PageResult<T> = Readonly<{ data: readonly T[]; total: number }>;\ntype LoadContext<TQuery> = Readonly<{ query: TQuery; signal: AbortSignal }>;\n\ntype PageSourceConfig<T, TFilter = unknown, TSort = unknown> = Readonly<{\n autoStart?: boolean;\n initialQuery?: PageQueryPatch<TFilter, TSort>;\n load(context: LoadContext<PageQuery<TFilter, TSort>>): Promise<PageResult<T>>;\n}>;\n\ntype PageSource<T, TFilter = unknown, TSort = unknown> = Source<T, PageQuery<TFilter, TSort>, PagePagination> & {\n readonly page: Readonly<{\n go(index: number): Promise<void>;\n last(): Promise<void>;\n next(): Promise<void>;\n previous(): Promise<void>;\n }>;\n reload(): Promise<void>;\n setQuery(changes: PageQueryPatch<TFilter, TSort>): Promise<void>;\n};\n```\n\n### Local sources\n\n```ts\ntype LocalQuery = Readonly<{ page: number; pageSize: number; search: string }>;\ntype LocalQueryPatch = Readonly<{ page?: number; pageSize?: number; search?: string }>;\ntype LocalSourceConfig<T> = Readonly<{\n initialQuery?: LocalQueryPatch;\n match?: (item: T, search: string) => boolean;\n}>;\n\ntype LocalSource<T> = Source<T, LocalQuery, PagePagination> & {\n readonly page: Readonly<{\n go(index: number): void;\n last(): void;\n next(): void;\n previous(): void;\n }>;\n setData(data: readonly T[]): void;\n setQuery(changes: LocalQueryPatch): void;\n};\n```\n\n### Cursor and infinite sources\n\n```ts\ntype CursorPagination<TCursor = string> = Readonly<{\n hasNext: boolean;\n hasPrevious: boolean;\n kind: 'cursor';\n nextCursor?: TCursor;\n previousCursor?: TCursor;\n total?: number;\n}>;\n\ntype CursorQuery<TCursor = string> = Readonly<{\n after?: TCursor;\n before?: TCursor;\n pageSize: number;\n search: string;\n}>;\n\ntype CursorQueryPatch<TCursor = string> = Readonly<{\n after?: TCursor | undefined;\n before?: TCursor | undefined;\n pageSize?: number;\n search?: string;\n}>;\n\ntype CursorResult<T, TCursor = string> = Readonly<{\n data: readonly T[];\n nextCursor?: TCursor;\n previousCursor?: TCursor;\n total?: number;\n}>;\n\ntype CursorSourceConfig<T, TCursor = string> = Readonly<{\n autoStart?: boolean;\n initialQuery?: CursorQueryPatch<TCursor>;\n load(context: LoadContext<CursorQuery<TCursor>>): Promise<CursorResult<T, TCursor>>;\n}>;\n\ntype CursorSource<T, TCursor = string> = Source<T, CursorQuery<TCursor>, CursorPagination<TCursor>> & {\n readonly page: Readonly<{ next(): Promise<void>; previous(): Promise<void> }>;\n reload(): Promise<void>;\n setQuery(changes: CursorQueryPatch<TCursor>): Promise<void>;\n};\n\ntype InfinitePagination = Readonly<{\n hasMore: boolean;\n isLoadingMore: boolean;\n kind: 'infinite';\n loaded: number;\n total: number;\n}>;\n\ntype InfiniteQuery = Readonly<{ pageSize: number; search: string }>;\ntype InfiniteQueryPatch = Readonly<{ pageSize?: number; search?: string }>;\n\ntype InfiniteSourceConfig<T> = Readonly<{\n autoStart?: boolean;\n initialQuery?: InfiniteQueryPatch;\n load(context: LoadContext<PageQuery>): Promise<PageResult<T>>;\n}>;\n\ntype InfiniteSource<T> = Source<T, InfiniteQuery, InfinitePagination> & {\n loadMore(): Promise<void>;\n reload(): Promise<void>;\n setQuery(changes: InfiniteQueryPatch): Promise<void>;\n};\n```\n\n### Shared helpers\n\n```ts\ntype AnyPagination = CursorPagination<unknown> | InfinitePagination | PagePagination;\n```\n",
|
|
6
|
+
"usage": "---\ntitle: Sourcerer — Usage Guide\ndescription: Build local, page, cursor, and infinite collection sources.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse a local source when data already exists in memory.\n\n```ts\nimport { createLocalSource } from '@vielzeug/sourcerer';\n\nconst source = createLocalSource(\n [\n { id: 1, name: 'Ada' },\n { id: 2, name: 'Grace' },\n { id: 3, name: 'Linus' },\n ],\n {\n initialQuery: { pageSize: 2 },\n match: (user, search) => user.name.toLowerCase().includes(search.toLowerCase()),\n },\n);\n\nsource.setQuery({ search: 'a' });\nconsole.log(source.snapshot.data);\nsource.dispose();\n```\n\nRead `snapshot.query`, `snapshot.data`, and `snapshot.pagination` together. They always describe one loaded result.\n\n## Handle Pending Remote Queries\n\nUse `pendingQuery` to distinguish loaded data from newer work.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst source = createPageSource<string>({\n autoStart: false,\n load: async ({ query }) => {\n const data = ['Ada', 'Grace', 'Linus'];\n const start = (query.page - 1) * query.pageSize;\n\n return { data: data.slice(start, start + query.pageSize), total: data.length };\n },\n});\n\nsource.subscribe((snapshot) => {\n if (snapshot.pendingQuery) console.log('Loading:', snapshot.pendingQuery);\n console.log('Loaded:', snapshot.query, snapshot.data);\n});\n\nawait source.setQuery({ page: 2 });\nsource.dispose();\n```\n\nNew `setQuery()` calls abort older requests. A failed current request preserves prior loaded data, records `snapshot.error`, and rejects the returned promise.\n\n## Use Cursor Pagination\n\nUse cursors when an API cannot provide stable page numbers.\n\n```ts\nimport { createCursorSource } from '@vielzeug/sourcerer';\n\nconst rows = ['A', 'B', 'C', 'D'];\nconst source = createCursorSource<string, number>({\n autoStart: false,\n initialQuery: { pageSize: 2 },\n load: async ({ query }) => {\n const start = query.after ?? 0;\n const data = rows.slice(start, start + query.pageSize);\n const nextCursor = start + data.length;\n\n return {\n data,\n nextCursor: nextCursor < rows.length ? nextCursor : undefined,\n previousCursor: start > 0 ? Math.max(0, start - query.pageSize) : undefined,\n };\n },\n});\n\nawait source.reload();\nawait source.page.next();\nconsole.log(source.snapshot.data);\nsource.dispose();\n```\n\n`after` and `before` cannot coexist. Search or page-size changes reset cursor state.\n\n## Build an Infinite Feed\n\nUse an infinite source when each page should append.\n\n```ts\nimport { createInfiniteSource } from '@vielzeug/sourcerer';\n\nconst source = createInfiniteSource<number>({\n autoStart: false,\n initialQuery: { pageSize: 2 },\n load: async ({ query }) => {\n const values = [1, 2, 3, 4, 5];\n const start = (query.page - 1) * query.pageSize;\n\n return { data: values.slice(start, start + query.pageSize), total: values.length };\n },\n});\n\nawait source.loadMore();\nawait source.loadMore();\nconsole.log(source.snapshot.data);\nsource.dispose();\n```\n\n`loadMore()` is a no-op while fetching or after `pagination.hasMore` becomes false.\n\n## Testing and Debugging\n\nInject deterministic loaders in unit tests. Await source commands before reading final state.\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nit('loads first page', async () => {\n const source = createPageSource({\n autoStart: false,\n load: async () => ({ data: ['Ada'], total: 1 }),\n });\n\n await source.reload();\n expect(source.snapshot.data).toEqual(['Ada']);\n source.dispose();\n});\n```\n\n## Framework Integration\n\nSubscribe through each framework’s lifecycle. Keep source creation stable across renders.\n\n::: code-group\n\n```tsx [React]\nimport { createPageSource } from '@vielzeug/sourcerer';\nimport { useEffect, useMemo, useSyncExternalStore } from 'react';\n\nexport function Users() {\n const source = useMemo(\n () => createPageSource({ load: async () => ({ data: [{ id: 1, name: 'Ada' }], total: 1 }) }),\n [],\n );\n const snapshot = useSyncExternalStore(source.subscribe, () => source.snapshot);\n\n useEffect(() => () => source.dispose(), [source]);\n\n return <p>{snapshot.isFetching ? 'Loading' : snapshot.data.length}</p>;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst source = createPageSource({ load: async () => ({ data: [{ id: 1, name: 'Ada' }], total: 1 }) });\nconst snapshot = shallowRef(source.snapshot);\nconst stop = source.subscribe((next) => (snapshot.value = next));\n\nonUnmounted(() => {\n stop();\n source.dispose();\n});\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createPageSource } from '@vielzeug/sourcerer';\n\n const source = createPageSource({ load: async () => ({ data: [{ id: 1, name: 'Ada' }], total: 1 }) });\n let snapshot = source.snapshot;\n const stop = source.subscribe((next) => (snapshot = next));\n\n onDestroy(() => {\n stop();\n source.dispose();\n });\n</script>\n\n{#if snapshot.isFetching}Loading{/if}\n{#each snapshot.data as user}{user.name}{/each}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Courier for transport policy. Sourcerer owns request succession; Courier owns HTTP behavior.\n\n```ts\nimport { createCourier } from '@vielzeug/courier';\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst courier = createCourier({ baseUrl: '/api' });\nconst source = createPageSource({\n load: ({ query, signal }) => courier.get('/users', { query, signal }),\n});\n```\n\nUse Scout’s matcher when local search needs an index.\n\n```ts\nimport { createIndex, toSearchMatcher } from '@vielzeug/scout';\nimport { createLocalSource } from '@vielzeug/sourcerer';\n\nconst users = [{ name: 'Ada' }, { name: 'Grace' }];\nconst index = createIndex(users, { fields: ['name'] });\nconst source = createLocalSource(users, { match: toSearchMatcher(index) });\n```\n\n## Best Practices\n\n- Dispose each source with its owning view, request, or scope.\n- Read one snapshot object per render instead of mixing source fields across updates.\n- Inspect `pendingQuery` before rendering controls for in-flight work.\n- Validate URL query values before passing them to `setQuery()`.\n- Keep caching, retries, polling, and optimistic writes in your transport layer.\n- Use `setData()` with prepared local collections; keep ranking and filtering explicit.\n- Debounce text inputs before updating remote source queries.\n",
|
|
7
|
+
"examples": "---\ntitle: Sourcerer — Examples\ndescription: Recipes for local, page, cursor, infinite, and framework source usage.\n---\n\n## Examples\n\n- [Local Pagination and Search](./examples/local-pagination-and-filtering.md)\n- [Page Query with URL State](./examples/remote-search-with-url-state.md)\n- [Cursor-Based Pagination](./examples/cursor-based-pagination.md)\n- [Infinite Scroll](./examples/infinite-scroll.md)\n- [Framework Integration](./examples/framework-integration.md)\n- [Remote Data with Courier](./examples/sourcerer-with-courier.md)\n- [Reactive Controls with Ripple](./examples/sourcerer-with-ripple.md)\n- [URL-Synced List with Wayfinder](./examples/sourcerer-with-wayfinder.md)\n"
|
|
8
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "cursor-source",
|
|
12
|
+
"code": "import { createCursorSource } from '@vielzeug/sourcerer'\n\nconst items = Array.from({ length: 30 }, (_, index) => ({ id: index + 1, label: `Item ${index + 1}` }))\n\nconst source = createCursorSource({\n initialQuery: { pageSize: 10 },\n load: async ({ query }) => {\n const start = query.after ? Number(query.after) : 0\n const data = items.slice(start, start + query.pageSize)\n const next = start + data.length\n return { data, nextCursor: next < items.length ? String(next) : undefined, previousCursor: start ? String(Math.max(0, start - query.pageSize)) : undefined }\n },\n})\n\nawait source.reload()\nawait source.page.next()\nconsole.log(source.snapshot.data.map((item) => item.label))\nconsole.log(source.snapshot.pagination)\n\nsource.dispose()",
|
|
13
|
+
"name": "Cursor Source"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "error-handling",
|
|
17
|
+
"code": "import { createPageSource } from '@vielzeug/sourcerer'\n\nconst source = createPageSource({\n autoStart: false,\n load: async () => { throw new Error('network down') },\n})\n\ntry {\n await source.reload()\n} catch (error) {\n console.log((error as Error).message)\n}\n\nconsole.log(source.snapshot.error?.message)\nconsole.log(source.snapshot.error?.message)\nsource.dispose()",
|
|
18
|
+
"name": "Error Handling"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "infinite-source",
|
|
22
|
+
"code": "import { createInfiniteSource } from '@vielzeug/sourcerer'\n\nconst posts = Array.from({ length: 25 }, (_, index) => ({ id: index + 1, title: `Post ${index + 1}` }))\n\nconst source = createInfiniteSource({\n initialQuery: { pageSize: 8 },\n load: async ({ query }) => {\n const start = (query.page - 1) * query.pageSize\n return { data: posts.slice(start, start + query.pageSize), total: posts.length }\n },\n})\n\nawait source.reload()\nawait source.loadMore()\nconsole.log(source.snapshot.data.length)\nconsole.log(source.snapshot.pagination)\n\nsource.dispose()",
|
|
23
|
+
"name": "Infinite Source"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "lifecycle",
|
|
27
|
+
"code": "import { createPageSource } from '@vielzeug/sourcerer'\n\nconst source = createPageSource({\n autoStart: false,\n load: async () => ({ data: ['item'], total: 1 }),\n})\n\nconsole.log(source.disposed)\nsource.disposalSignal.addEventListener('abort', () => console.log('disposed'))\nawait source.reload()\nconsole.log(source.snapshot.data)\nsource.dispose()\nconsole.log(source.disposalSignal.aborted)",
|
|
28
|
+
"name": "Source Lifecycle"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "local-source",
|
|
32
|
+
"code": "import { createLocalSource } from '@vielzeug/sourcerer'\n\nconst users = [\n { id: 1, name: 'Ada', role: 'admin' },\n { id: 2, name: 'Grace', role: 'admin' },\n { id: 3, name: 'Linus', role: 'user' },\n]\n\nconst source = createLocalSource(users, {\n initialQuery: { pageSize: 2 },\n match: (user, search) => user.name.toLowerCase().includes(search.toLowerCase()),\n})\n\nsource.setQuery({ search: 'a' })\nconsole.log(source.snapshot.data)\nconsole.log(source.snapshot.pagination)\n\nsource.dispose()",
|
|
33
|
+
"name": "Local Source"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "page-source",
|
|
37
|
+
"code": "import { createPageSource } from '@vielzeug/sourcerer'\n\nconst allItems = Array.from({ length: 47 }, (_, index) => ({ id: index + 1, name: `Item ${index + 1}` }))\n\nconst source = createPageSource({\n initialQuery: { pageSize: 10 },\n load: async ({ query }) => {\n const filtered = query.search ? allItems.filter((item) => item.name.includes(query.search)) : allItems\n const start = (query.page - 1) * query.pageSize\n return { data: filtered.slice(start, start + query.pageSize), total: filtered.length }\n },\n})\n\nawait source.reload()\nawait source.setQuery({ search: 'Item 4' })\nconsole.log(source.snapshot.data.map((item) => item.name))\nconsole.log(source.snapshot.pagination)\n\nsource.dispose()",
|
|
38
|
+
"name": "Page Source"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"typeSignatures": {
|
|
42
|
+
"createCursorSource": "export { createCursorSource } from './cursorSource';",
|
|
43
|
+
"createInfiniteSource": "export { createInfiniteSource } from './infiniteSource';",
|
|
44
|
+
"createLocalSource": "export { createLocalSource } from './localSource';",
|
|
45
|
+
"createPageSource": "export { createPageSource } from './pageSource';",
|
|
46
|
+
"AnyPagination": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
47
|
+
"CursorPagination": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
48
|
+
"CursorQuery": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
49
|
+
"CursorQueryPatch": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
50
|
+
"CursorResult": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
51
|
+
"CursorSource": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
52
|
+
"CursorSourceConfig": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
53
|
+
"InfinitePagination": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
54
|
+
"InfiniteQuery": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
55
|
+
"InfiniteQueryPatch": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
56
|
+
"InfiniteSource": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
57
|
+
"InfiniteSourceConfig": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
58
|
+
"LoadContext": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
59
|
+
"LocalQuery": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
60
|
+
"LocalQueryPatch": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
61
|
+
"LocalSource": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
62
|
+
"LocalSourceConfig": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
63
|
+
"PagePagination": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
64
|
+
"PageQuery": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
65
|
+
"PageQueryPatch": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
66
|
+
"PageResult": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
67
|
+
"PageSource": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
68
|
+
"PageSourceConfig": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
69
|
+
"Source": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';",
|
|
70
|
+
"SourceSnapshot": "export type {\n AnyPagination,\n CursorPagination,\n CursorQuery,\n CursorQueryPatch,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n InfinitePagination,\n InfiniteQuery,\n InfiniteQueryPatch,\n InfiniteSource,\n InfiniteSourceConfig,\n LoadContext,\n LocalQuery,\n LocalQueryPatch,\n LocalSource,\n LocalSourceConfig,\n PagePagination,\n PageQuery,\n PageQueryPatch,\n PageResult,\n PageSource,\n PageSourceConfig,\n Source,\n SourceSnapshot,\n} from './types';"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "import { fail, prependIssuePath } from './errors';\nimport { createParseContext } from './messages';\n\nexport type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';\nexport {\n ErrorCode,\n PipeSchema,\n Schema,\n SpellDefinitionError,\n SpellError,\n SpellValidationError,\n schemaMode,\n} from './core';\nexport type { DeepPartial } from './messages';\nexport { s } from './s';\n\n/** Error helpers and immutable parse-context creation are secondary operations. */\nexport const diagnostics = {\n createParseContext,\n fail,\n prependIssuePath,\n};\n",
|
|
3
|
+
"docs": {
|
|
4
|
+
"index": "---\ntitle: Spell — Schema validation for TypeScript\ndescription: Schema validation with explicit sync/async checks, portable definitions, JSON Schema export, and tree-shakeable entry points.\npackage: spell\ncategory: validation\nkeywords: [schema, validation, parsing, json-schema, locale, typescript, descriptors]\nrelated: [forge, courier, vault]\nexports:\n [s, Schema, PipeSchema, SpellValidationError, SpellDefinitionError, ErrorCode, diagnostics, './json', './predicates']\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"spell\" />\n\n## Why Spell?\n\nSpell keeps runtime validation, static inference, and portable definitions in one API. Use `s` for schema construction; import JSON conversion and predicates from dedicated subpaths.\n\nThis example shows the difference between manual branching and a single reusable schema.\n\n```ts\n// Before\nfunction parseUserBefore(value: unknown) {\n if (typeof value !== 'object' || value === null) throw new Error('Expected object');\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.email !== 'string' || !candidate.email.includes('@')) {\n throw new Error('Expected valid email');\n }\n\n if (typeof candidate.role !== 'string' || !['admin', 'editor', 'viewer'].includes(candidate.role)) {\n throw new Error('Expected valid role');\n }\n\n return {\n email: candidate.email,\n role: candidate.role,\n };\n}\n\n// After\nimport { s } from '@vielzeug/spell';\n\nconst User = s.object({\n email: s.string().email(),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n});\n\nconst user = User.parse({ email: 'ada@example.com', role: 'admin' });\n```\n\n| Feature | Spell | Zod | Yup |\n| ----------------- | --------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------- |\n| Bundle size | <PackageInfo package=\"spell\" type=\"size\" /> | ~62 kB | ~14 kB |\n| Type inference | <ore-icon name=\"check\" size=\"16\"></ore-icon> `Infer<T>` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Coercion API | <ore-icon name=\"check\" size=\"16\"></ore-icon> `s.coerce.*` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Async validation | <ore-icon name=\"check\" size=\"16\"></ore-icon> `.checkAsync()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Error flattening | <ore-icon name=\"check\" size=\"16\"></ore-icon> `flatten()` + `flattenFirst()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | 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 Spell when** you want a fluent schema API with strong TypeScript inference, structured errors, and no third-party runtime dependencies.\n\n**Consider alternatives when** you are already standardized on another validator ecosystem and migration cost outweighs the API benefits.\n\n</div>\n\n## Installation\n\nUse your workspace package manager to add Spell.\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/spell\n```\n\n```sh [npm]\nnpm install @vielzeug/spell\n```\n\n```sh [yarn]\nyarn add @vielzeug/spell\n```\n\n:::\n\n## Quick Start\n\nStart with a schema, then parse unknown input and use the inferred output type everywhere else.\n\n```ts\nimport { s, type Infer } from '@vielzeug/spell';\n\nconst User = s\n .object({\n email: s.string().email(),\n name: s.string().min(1),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n })\n .relaxed(); // allow extra keys — omit for strict-mode (default)\n\ntype User = Infer<typeof User>;\n\nconst payload: unknown = {\n email: 'ada@example.com',\n name: 'Ada',\n role: 'admin',\n team: 'platform',\n};\n\nconst result = User.safeParse(payload);\n\nif (!result.success) throw result.error;\nconst user = result.data;\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Namespace and tree-shakeable schema builders.\n- Sync and async parsing with `parse()`, `safeParse()`, `parseAsync()`, and `safeParseAsync()`.\n- Explicit `check()` and `checkAsync()` rules; sync parsing never skips an async check.\n- Wrapper modes for `optional`, `nullable`, `nullish`, `default`, `catch`, and `required`.\n- Frozen declarative definitions through `definition()` and JSON Schema export via `fromDefinition()` from `@vielzeug/spell/json`.\n- Grouped `diagnostics` and `predicates` utilities keep schema construction focused.\n- Ordered union parsing produces the same selected branch in sync and async modes.\n- Structured errors with direct path lookup, flattened views, and best-match union diagnostics.\n- Object parsing is hardened against prototype-pollution-style keys.\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- [Forge](/forge/) — typed form state that uses Spell schemas as its validation layer\n- [Courier](/courier/) — HTTP client for validating request and response payloads at service boundaries\n- [Vault](/vault/) — unified storage API that accepts Spell schemas to type-gate persisted data\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Spell — API Reference\ndescription: Reference for Spell schema builders, parsing, diagnostics, and tooling exports.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ----------------------- | ------------------------------- | ---------------------------------- | ---------------------------------------------------- |\n| `s` | Creates schemas | Sync or async, depending on checks | `checkAsync()` requires async parsing |\n| `Schema` / `PipeSchema` | Base schema abstractions | Sync or async | Use `Infer` rather than assuming input equals output |\n| `diagnostics` | Parse-context and error helpers | Sync | Context is per parse/request, not global |\n| `SpellValidationError` | Validation failure details | Sync/async parse failures | Use `safeParse()` to handle it as a result |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------------------- | ----------------------------------------------- |\n| `@vielzeug/spell` | Schema builders, errors, types, and diagnostics |\n| `@vielzeug/spell/json` | Convert portable definitions to JSON Schema |\n| `@vielzeug/spell/predicates` | Standalone format and type predicates |\n\n```ts\nimport { diagnostics, s, type Infer } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\nimport { isEmail } from '@vielzeug/spell/predicates';\n```\n\n## `s`\n\nAll builders live under `s`.\n\n| Builder | Purpose |\n| ----------------------------------------------------------------- | -------------------------- |\n| `string`, `number`, `boolean`, `bigint`, `date` | Primitive values |\n| `literal`, `enum`, `null`, `undefined`, `unknown`, `any`, `never` | Exact and universal values |\n| `array`, `tuple`, `set`, `map`, `record`, `object` | Collections |\n| `union`, `intersect`, `discriminatedUnion`, `lazy` | Composition |\n| `coerce.*` | Coercing primitive schemas |\n\n```ts\nconst User = s.object({\n email: s.string().email(),\n id: s.string().uuid(),\n role: s.enum(['admin', 'member'] as const),\n});\n\ntype User = Infer<typeof User>;\n```\n\nObject schemas reject unknown keys. Use `.relaxed()` to retain extras.\n\n## Parsing\n\nEvery schema provides:\n\n```ts\nschema.parse(value, context?); // Output or SpellValidationError\nschema.safeParse(value, context?); // ParseResult<Output>\nschema.parseAsync(value, context?); // Promise<Output>\nschema.safeParseAsync(value, context?); // Promise<ParseResult<Output>>\nschema.is(value); // value is Output\nschema.assert(value, label?); // assertion\n```\n\n`parse()` and `safeParse()` are available on synchronous schemas. Calling `checkAsync()` returns an async-only schema, where TypeScript exposes only `parseAsync()` and `safeParseAsync()`. That async-only mode propagates through compositional schemas when a child is asynchronous.\n\n## Custom Checks\n\n`check()` is synchronous. `checkAsync()` is asynchronous. Do not return a Promise from `check()`.\n\n```ts\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((value, context) => {\n if (value.password !== value.confirm) {\n context.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n\nconst AvailableEmail = s\n .string()\n .email()\n .checkAsync(async (value) => {\n return (await emailAvailable(value)) || 'Email is already registered';\n });\n```\n\n`CheckContext.addIssue()` takes `{ code, message, params?, path? }`. Paths are relative to current schema.\n\n## Modifiers and Transforms\n\n```ts\ns.string().optional();\ns.string().nullable();\ns.string().nullish();\ns.string().required();\ns.string().default('guest');\ns.string().catch('guest');\ns.string()\n .trim()\n .transform((value) => value.toLowerCase());\ns.string().pipe(s.string().slug());\ns.string().label('User name');\n```\n\n`default()`, `catch()`, preprocessors, transforms, and checks are runtime behavior. They cannot become portable definitions.\n\n## Definitions and JSON Schema\n\n`definition()` is only for schemas containing declarative structure. It returns frozen data and throws `SpellDefinitionError` when runtime behavior is present.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1),\n});\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n```\n\nNo implicit schema-to-JSON conversion exists. Make definition boundary explicit.\n\n## Diagnostics\n\n`diagnostics` contains pure helpers and immutable parse-context creation.\n\n```ts\nimport { diagnostics, s } from '@vielzeug/spell';\n\nconst context = diagnostics.createParseContext({\n object: { invalidKeys: () => 'Unsupported field' },\n});\n\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'ada@example.com', extra: true }, context);\n\nif (!result.success) {\n const messages = result.error.messagesAt('email');\n console.log(messages);\n}\n```\n\n`diagnostics.fail(code, message, params?)` and `diagnostics.prependIssuePath(issues, segment)` support custom parser implementations.\n\n## Errors\n\n- `SpellError` — base class. Use `SpellError.is(error)` for cross-boundary narrowing.\n- `SpellValidationError` — validation failure with `issues`, `bestMatch()`, `messagesAt()`, `flatten()`, and `flattenFirst()`.\n- `SpellDefinitionError` — schema cannot create portable definition.\n\n```ts\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'invalid' });\n\nif (!result.success) {\n const { fieldErrors, formErrors } = result.error.flatten();\n console.log(fieldErrors, formErrors);\n}\n```\n\n## Types\n\n### Core schema types\n\n```ts\ntype SchemaMode = 'async' | 'sync';\n\ntype AnySchema<Output = unknown, Input = Output, Mode extends SchemaMode = SchemaMode> = SchemaSurface<\n Output,\n Input,\n Mode\n>;\n\ntype SchemaSurface<Output = unknown, Input = Output, Mode extends SchemaMode = SchemaMode> = {\n _parseFullAsync(value: unknown, ctx?: ParseContext): Promise<{ data: unknown; issues: Issue[] }>;\n _parseFullSync(value: unknown, ctx?: ParseContext): { data: unknown; issues: Issue[] };\n definition(): SchemaDescriptor;\n isOptional: boolean;\n optional(): SchemaSurface<Output | undefined, Input | undefined, Mode>;\n required(): SchemaSurface<Exclude<Output, undefined>, Exclude<Input, undefined>, Mode>;\n readonly [schemaInput]: Input;\n readonly [schemaMode]: Mode;\n readonly [schemaOutput]: Output;\n walk<R>(visitor: SchemaWalker<R>): R | null;\n};\n```\n\n`schemaMode` is the public symbol marking a schema's parsing capability.\n\n### Inference types\n\n```ts\ntype InferOutput<T> =\n T extends Schema<infer Output, unknown, SchemaMode>\n ? Output\n : T extends { readonly [schemaOutput]: infer Output }\n ? Output\n : never;\ntype InferInput<T> = T extends { readonly [schemaInput]: infer Input } ? Input : unknown;\ntype Infer<T> = InferOutput<T>;\ntype InferSchemaMode<T> = T extends { readonly [schemaMode]: infer Mode extends SchemaMode } ? Mode : never;\ntype MergeSchemaModes<Modes extends SchemaMode> = 'async' extends Modes ? 'async' : 'sync';\n```\n\n### Parse result and issues\n\n```ts\ntype ParseResult<T> = { data: T; success: true } | { error: SpellValidationError; success: false };\n\ntype Issue =\n | { code: 'custom'; message: string; params?: Record<string, unknown>; path: (string | number)[] }\n | { code: 'invalid_base64'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_date'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_duration'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_enum'; message: string; params: { values: readonly unknown[] }; path: (string | number)[] }\n | { code: 'invalid_finite'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_integer'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_keys'; message: string; params: { keys: string[] }; path: (string | number)[] }\n | { code: 'invalid_length'; message: string; params: { exact: number }; path: (string | number)[] }\n | { code: 'invalid_literal'; message: string; params: { expected: unknown }; path: (string | number)[] }\n | { code: 'invalid_multiple_of'; message: string; params: { step: number | bigint }; path: (string | number)[] }\n | { code: 'invalid_safe'; message: string; params?: undefined; path: (string | number)[] }\n | {\n code: 'invalid_string';\n message: string;\n params: { format?: string; includes?: string; pattern?: string; prefix?: string; suffix?: string };\n path: (string | number)[];\n }\n | { code: 'invalid_type'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_union'; message: string; params: { errors: Issue[][] }; path: (string | number)[] }\n | { code: 'invalid_unique'; message: string; params: { unique: true }; path: (string | number)[] }\n | { code: 'invalid_url'; message: string; params: { format: string }; path: (string | number)[] }\n | {\n code: 'invalid_variant';\n message: string;\n params: { discriminator: string; expected: string[] };\n path: (string | number)[];\n }\n | {\n code: 'too_big';\n message: string;\n params: { exclusive?: boolean; max: number | bigint | Date };\n path: (string | number)[];\n }\n | {\n code: 'too_small';\n message: string;\n params: { exclusive?: boolean; min: number | bigint | Date };\n path: (string | number)[];\n }\n | { code: string & {}; message: string; params?: Record<string, unknown>; path: (string | number)[] };\n```\n\n`ErrorCode` is a const object mapping each issue code to its string literal.\n\n### Validation contracts\n\n```ts\ntype ParseContext = { messages: Messages };\n\ntype ValidateFn = (value: unknown, ctx?: ParseContext) => Issue[] | null | Promise<Issue[] | null>;\n\ntype CheckContext = {\n addIssue: (issue: {\n code: string;\n message: string;\n params?: Record<string, unknown>;\n path?: (string | number)[];\n }) => void;\n};\n\ntype ValidateResult = boolean | null | undefined | string;\n```\n\n### Messages\n\n```ts\ntype MessageFn<Ctx extends Record<string, unknown> = Record<string, unknown>> = string | ((ctx: Ctx) => string);\n\ntype Messages = {\n array: { length: (ctx: { exact: number; value: unknown[] }) => string; max: (ctx: { max: number; value: unknown[] }) => string; min: (ctx: { min: number; value: unknown[] }) => string; nonEmpty: () => string; type: () => string; unique: () => string };\n bigint: { max: (ctx: { max: bigint; value: bigint }) => string; min: (ctx: { min: bigint; value: bigint }) => string; multipleOf: (ctx: { step: bigint; value: bigint }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; type: () => string };\n boolean: { type: () => string };\n check: { default: () => string };\n date: { max: (ctx: { max: Date; value: Date }) => string; min: (ctx: { min: Date; value: Date }) => string; type: () => string };\n enum: { invalid: (ctx: { values: readonly unknown[] }) => string };\n instanceof: { type: (ctx: { className: string }) => string };\n literal: { expected: (ctx: { expected: unknown }) => string };\n map: { max: (ctx: { max: number; value: Map<unknown, unknown> }) => string; min: (ctx: { min: number; value: Map<unknown, unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Map<unknown, unknown> }) => string; type: () => string };\n never: { invalid: () => string };\n number: { finite: () => string; int: () => string; max: (ctx: { max: number; value: number }) => string; min: (ctx: { min: number; value: number }) => string; multipleOf: (ctx: { step: number; value: number }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; safe: () => string; type: () => string };\n object: { invalidKeys: (ctx: { keys: string[] }) => string; type: () => string };\n set: { max: (ctx: { max: number; value: Set<unknown> }) => string; min: (ctx: { min: number; value: Set<unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Set<unknown> }) => string; type: () => string };\n string: { base64: () => string; base64url: () => string; cuid: () => string; cuid2: () => string; date: () => string; dateTime: () => string; duration: () => string; email: () => string; emoji: () => string; endsWith: (ctx: { suffix: string; value: string }) => string; hex: () => string; hexColor: () => string; includes: (ctx: { substr: string; value: string }) => string; ip: () => string; jwt: () => string; length: (ctx: { exact: number; value: string }) => string; max: (ctx: { max: number; value: string }) => string; min: (ctx: { min: number; value: string }) => string; nanoid: () => string; nonEmpty: () => string; numeric: () => string; regex: (ctx: { value: string }) => string; semver: () => string; slug: () => string; startsWith: (ctx: { prefix: string; value: string }) => string; time: () => string; type: () => string; ulid: () => string; url: () => string; uuid: () => string };\n tuple: { length: (ctx: { exact: number }) => string; min: (ctx: { min: number }) => string; type: () => string };\n union: { invalid: () => string };\n variant: { invalidDiscriminator: (ctx: { discriminator: string; expected: string[] }) => string; type: () => string };\n};\n\ntype DeepPartial<T> = {\n [K in keyof T]?: T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K];\n};\n```\n\n### Descriptor and JSON Schema\n\n```ts\ntype SchemaDescriptor = BaseDescriptor &\n (\n | { kind: 'any' | 'unknown' | 'never' | 'boolean' | 'bigint' | 'date' | 'lazy' }\n | { className: string; kind: 'instanceof' }\n | { contentEncoding?: string; format?: string; kind: 'string'; maxLength?: number; minLength?: number; pattern?: string | null }\n | { exclusiveMaximum?: number; exclusiveMinimum?: number; kind: 'number'; maximum?: number; minimum?: number; multipleOf?: number; typeHint?: 'integer' }\n | { kind: 'literal'; value: string | number | boolean | null | undefined }\n | { kind: 'enum'; values: readonly (string | number)[] }\n | { items: SchemaDescriptor; kind: 'array'; maxItems?: number; minItems?: number }\n | { items: SchemaDescriptor[]; kind: 'tuple'; rest: SchemaDescriptor | null }\n | { fields: Record<string, SchemaDescriptor>; kind: 'object'; strict: boolean }\n | { key: SchemaDescriptor; kind: 'record'; value: SchemaDescriptor }\n | { items: SchemaDescriptor; kind: 'set' }\n | { key: SchemaDescriptor; kind: 'map'; value: SchemaDescriptor }\n | { branches: SchemaDescriptor[]; kind: 'union' | 'intersect' }\n | { branches: Record<string, SchemaDescriptor>; discriminator: string; kind: 'variant' }\n | { from: SchemaDescriptor; kind: 'pipe'; to: SchemaDescriptor }\n );\n\ntype JsonSchema = Record<string, unknown>;\n```\n\n### Schema walker\n\n```ts\ntype SchemaWalker<R> = {\n array?: <T extends AnySchema, Mode extends SchemaMode>(schema: ArraySchema<T, Mode>, item: R | null) => R;\n bigint?: <Input, Mode extends SchemaMode>(schema: BigIntSchema<Input, Mode>) => R;\n boolean?: <Input, Mode extends SchemaMode>(schema: BooleanSchema<Input, Mode>) => R;\n date?: <Input, Mode extends SchemaMode>(schema: DateSchema<Input, Mode>) => R;\n enum?: <T extends EnumValues, Mode extends SchemaMode>(schema: EnumSchema<T, Mode>) => R;\n instanceof?: <T, Mode extends SchemaMode>(schema: InstanceOfSchema<T, Mode>) => R;\n intersect?: <T extends readonly AnySchema[], Mode extends SchemaMode>(schema: IntersectSchema<T, Mode>, branches: (R | null)[]) => R;\n lazy?: <T, Input, Mode extends SchemaMode>(schema: LazySchema<T, Input, Mode>) => R;\n literal?: <T extends string | number | boolean | null | undefined, Mode extends SchemaMode>(schema: LiteralSchema<T, Mode>) => R;\n map?: <K extends AnySchema, V extends AnySchema, Mode extends SchemaMode>(schema: MapSchema<K, V, Mode>, key: R | null, value: R | null) => R;\n never?: <Mode extends SchemaMode>(schema: NeverSchema<Mode>) => R;\n number?: <Input, Mode extends SchemaMode>(schema: NumberSchema<Input, Mode>) => R;\n object?: <T extends ObjectShape, Mode extends SchemaMode>(schema: ObjectSchema<T, Mode>, fields: Record<string, R | null>) => R;\n pipe?: <To extends AnySchema, From extends AnySchema, Mode extends SchemaMode>(schema: PipeSchema<To, From, Mode>, from: R | null, to: R | null) => R;\n record?: <K extends AnySchema, V extends AnySchema, Mode extends SchemaMode>(schema: RecordSchema<K, V, Mode>, key: R | null, value: R | null) => R;\n set?: <T extends AnySchema, Mode extends SchemaMode>(schema: SetSchema<T, Mode>, item: R | null) => R;\n string?: <Input, Mode extends SchemaMode>(schema: StringSchema<Input, Mode>) => R;\n tuple?: <T extends TupleSchemas, Rest extends AnySchema | null, Mode extends SchemaMode>(schema: TupleSchema<T, Rest, Mode>, items: (R | null)[], rest: R | null) => R;\n union?: <T extends readonly AnySchema[], Mode extends SchemaMode>(schema: UnionSchema<T, Mode>, branches: (R | null)[]) => R;\n unknown?: (schema: AnySchema) => R;\n variant?: <K extends string, M extends Record<string, ObjectSchema<any, any>>, Mode extends SchemaMode>(schema: VariantSchema<K, M, Mode>, branches: Record<string, R | null>) => R;\n};\n```\n\n### Error helpers\n\n```ts\ntype FlatError = { messages: string[]; path: (string | number)[] };\ntype FlatErrorFirst = { message: string; path: (string | number)[] };\n```\n",
|
|
6
|
+
"usage": "---\ntitle: Spell — Usage Guide\ndescription: Learn how to build schemas, compose wrappers, customize locales, and integrate spell with other Vielzeug packages.\n---\n\n[[toc]]\n\n## Basic Usage\n\nStart with `safeParse()` when you want explicit success and failure branches.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Signup = s.object({\n email: s.string().email(),\n password: s.string().min(12),\n referralCode: s.string().optional(),\n});\n\nconst result = Signup.safeParse({\n email: 'ada@example.com',\n password: 'horse-battery-staple',\n});\n\nif (!result.success) {\n console.error(result.error.issues);\n} else {\n console.log(result.data.email);\n}\n```\n\nUse `parse()` when invalid input should throw immediately. Use `safeParse()` when invalid input is part of normal control flow.\n\n## Building Schemas\n\nUse the namespace form when readability matters more than bundle trimming.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Article = s.object({\n id: s.string().uuid(),\n title: s.string().trim().min(1).max(120),\n slug: s.string().slug(),\n tags: s.array(s.string().min(1)).default(() => []),\n meta: s\n .object({\n published: s.boolean(),\n publishedAt: s.date().nullable(),\n })\n .relaxed(),\n});\n```\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Todo = s.object({\n done: s.boolean(),\n tags: s.array(s.string().min(1)).default(() => []),\n title: s.string().min(1),\n});\n```\n\nObject schemas reject unknown keys by default. Call `.relaxed()` when you need to preserve extra properties.\n\nCall `.defaults()` to get a fully default-filled object without providing any input. Every required field must have a `.default()` set, or a `SpellValidationError` is thrown. Call `.partialDefaults()` when only some fields have defaults — fields without a default are silently omitted instead of throwing.\n\n```ts\nconst Config = s.object({\n host: s.string().default('localhost'),\n port: s.number().default(3000),\n});\n\nConfig.defaults(); // { host: 'localhost', port: 3000 }\n\nconst Form = s.object({ name: s.string(), role: s.string().default('viewer') });\nForm.partialDefaults(); // { role: 'viewer' }\n```\n\n## Wrapper Modes, Defaults, and Fallbacks\n\nChain wrappers to describe missing values and recovery rules without losing schema metadata.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst DisplayName = s.string().trim().min(2).label('Display name').optional().default('Guest').nullable();\n\nDisplayName.parse(undefined); // 'Guest'\nDisplayName.parse(null); // null\nDisplayName.description; // 'Display name'\n```\n\nCall `.required()` to remove `undefined` without removing `null`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst NullableButRequired = s.string().optional().nullable().required();\n\nNullableButRequired.parse('Ada');\nNullableButRequired.parse(null);\n// NullableButRequired.parse(undefined); // throws\n```\n\nUse `.catch()` when you want a fallback output after validation fails.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Port = s.number().int().min(1).max(65535).catch(3000);\n\nPort.parse('not-a-number'); // 3000\n```\n\n## Custom Validation\n\nUse `check()` for synchronous domain rules and `checkAsync()` for asynchronous rules. Sync parsing rejects schemas with asynchronous checks.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\n// Boolean shorthand: return false to fail with default message\nconst EvenNumber = s.number().check((n) => n % 2 === 0);\n\n// String shorthand: return the message as a string\nconst Username = s\n .string()\n .min(3)\n .check((v) => !v.startsWith('_') || 'Cannot start with underscore');\n\n// Multiple issues via ctx.addIssue()\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((v, ctx) => {\n if (v.password !== v.confirm) {\n ctx.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n```\n\n`checkAsync()` returns an async-only schema: TypeScript exposes `parseAsync()` and `safeParseAsync()` but not `parse()` or `safeParse()`. This mode survives fluent modifiers and propagates through nested arrays, objects, unions, intersections, tuples, maps, records, sets, lazy schemas, pipelines, and `s.discriminatedUnion(...)` branches. Sync parsing also fails at runtime instead of accepting an unchecked value.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst takenEmails = new Set(['ada@example.com']);\n\nconst AccountEmail = s\n .string()\n .email()\n .checkAsync(async (value, ctx) => {\n if (takenEmails.has(value)) {\n ctx.addIssue({ code: 'custom', message: 'Email is already taken', path: [] });\n }\n });\n\n// Async checks require parseAsync\nawait AccountEmail.parseAsync('grace@example.com');\n```\n\nUse `check()` for predicate-only rules too. Return `true` on success or message on failure.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst PositivePrice = s.number().check((value) => value > 0 || 'Must be positive');\nPositivePrice.parse(9.99);\n```\n\n## Strings, Numbers, and Safe Regex Usage\n\nUse schema helpers for common string and number constraints instead of hand-written predicates.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Password = s.string().min(12).regex(/[A-Z]/).regex(/[0-9]/);\nconst Price = s.number().nonNegative().multipleOf(0.01);\nconst LaunchWindow = s.date().min(new Date('2025-01-01T00:00:00.000Z'));\n```\n\nSpell strips stateful `/g` and `/y` flags from `regex()` patterns before validation. Repeated parses stay deterministic even when the original regular expression is reused.\n\n## Coercion and Transforms\n\nUse coercion when input arrives as strings, query parameters, or form values.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Query = s.object({\n draft: s.coerce.boolean().default(false),\n limit: s.coerce.number().int().positive().default(20),\n publishedAt: s.coerce.date().nullable(),\n search: s.coerce.string().trim().min(1).optional(),\n});\n\nconst parsed = Query.parse({\n draft: 'true',\n limit: '50',\n publishedAt: '2025-04-01T12:00:00.000Z',\n search: ' vielzeug ',\n});\n```\n\nUse `transform()` or `pipe()` after validation when downstream code needs a different output shape.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst TrimmedTags = s.array(s.string().trim().min(1)).transform((tags) => tags.map((tag) => tag.toLowerCase()));\nconst Slug = s.string().trim().min(1).pipe(s.string().slug());\n```\n\n## Introspection, Round-Trips, and JSON Schema\n\nUse declarative definitions when schemas need to cross process boundaries or feed tooling.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s\n .object({\n id: s.string().uuid(),\n name: s.string().min(1),\n price: s.number().positive().multipleOf(0.01),\n })\n .label('Product');\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n\nProduct.parse({ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Keyboard', price: 129.99 });\nconsole.log(jsonSchema.title);\n```\n\nDefinitions are frozen serializable snapshots of declarative schema structure. Use `definition()` and `fromDefinition()` for external tooling. Schemas with runtime checks, transforms, defaults, catches, or preprocessors intentionally have no definition.\n\n## Messages\n\nSpell has no mutable process-wide configuration. Build one parse context per request, locale, or form, then pass it explicitly.\n\n```ts\nimport { diagnostics, s } from '@vielzeug/spell';\n\nconst User = s.object({ email: s.string().email() });\nconst german = diagnostics.createParseContext({\n object: { invalidKeys: () => 'Keine unbekannten Felder erlaubt' },\n});\n\nUser.safeParse({ email: 'ada@example.com', extra: true }, german);\n```\n\nInternal development warnings always use `console.warn` in development builds. Route application diagnostics in application code instead of mutating library-wide logger state.\n\n## Working with Validation Errors\n\nUse `SpellValidationError` helpers when you need UI-ready error structures.\n\n```ts\nimport { s, SpellValidationError } from '@vielzeug/spell';\n\nconst User = s.object({\n email: s.string().email(),\n profile: s.object({\n name: s.string().min(2),\n }),\n});\n\nconst result = User.safeParse({ email: 'nope', profile: { name: '' } });\n\nif (!result.success && result.error instanceof SpellValidationError) {\n const profileErrors = result.error.messagesAt('profile', 'name');\n console.log(profileErrors);\n}\n```\n\nUse `bestMatch()` on a union failure when you want the branch that came closest to succeeding. Pass a specific `invalid_union` issue when one validation produced multiple union failures.\n\n## Schema Traversal with walk()\n\nUse `walk()` to inspect or transform a schema tree without importing internal implementation classes.\n\n```ts\nimport { s, type SchemaWalker } from '@vielzeug/spell';\n\nconst fields: string[] = [];\n\nconst collectFields: SchemaWalker<void> = {\n object(schema) {\n for (const [key, child] of Object.entries(schema.shape)) {\n fields.push(key);\n child.walk(collectFields);\n }\n },\n unknown() {},\n};\n\nconst User = s.object({\n email: s.string().email(),\n profile: s.object({ name: s.string() }),\n});\n\nUser.walk(collectFields);\nconsole.log(fields); // ['email', 'profile', 'name']\n```\n\n`walk()` dispatches by `schema.kind`. If no handler matches and no `unknown` fallback is provided, `walk()` returns `null`. Add an `unknown` handler to capture any kind not explicitly listed in your visitor.\n\n## Framework Integration\n\nSpell works anywhere you can call a function before state enters your app.\n\n::: code-group\n\n```tsx [React]\nimport { s } from '@vielzeug/spell';\n\nconst SearchParams = s\n .object({\n page: s.coerce.number().int().positive().default(1),\n q: s.string().trim().optional(),\n })\n .relaxed();\n\nexport function SearchPage({ rawParams }: { rawParams: unknown }) {\n const params = SearchParams.parse(rawParams);\n\n return (\n <div>\n {params.q ?? 'All results'} — page {params.page}\n </div>\n );\n}\n```\n\n```ts [Vue]\nimport { computed, ref } from 'vue';\nimport { s } from '@vielzeug/spell';\n\nconst Settings = s.object({\n locale: s.string().min(2),\n compact: s.coerce.boolean().default(false),\n});\n\nconst raw = ref<unknown>({ locale: 'en', compact: 'true' });\nconst settings = computed(() => Settings.parse(raw.value));\n```\n\n:::\n\nUse `safeParse()` at event boundaries and `parse()` inside trusted data flows.\n\n## Working with Other Vielzeug Libraries\n\nUse Spell as the validation layer and let other packages focus on transport, forms, or storage.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { createCourier } from '@vielzeug/courier';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({\n displayName: s.string().min(2),\n newsletter: s.boolean(),\n});\n\nconst form = createForm({\n initialValues: {\n displayName: '',\n newsletter: false,\n },\n validate: customValidator(Profile),\n});\n\nconst courier = createCourier({ baseUrl: '/api' });\nconst profile = Profile.parse(await courier.get('/profile'));\n```\n\nUse Spell definitions with `@vielzeug/codex` or other tooling when you need generated docs or external schema consumers.\n\n## Best Practices\n\n- Keep schemas close to the boundary where unknown data enters your app.\n- Use `s` consistently for construction; use explicit `/json` and `/predicates` subpaths for tooling.\n- Use `.default(() => value)` for mutable defaults such as arrays, objects, `Map`, and `Set`.\n- Call `.required()` when you want to remove `undefined` but keep `null` semantics intact.\n- Use `check()` with a `ctx` argument when you need `ctx.addIssue()`; return a message for simple predicate failures.\n- Use `checkAsync()` and `parseAsync()` for every asynchronous domain rule.\n- Build a parse context per request or test; never rely on mutable process-wide configuration.\n- Use `definition()` with `fromDefinition()` from `@vielzeug/spell/json` for external tooling.\n",
|
|
7
|
+
"examples": "---\ntitle: Spell — Examples\ndescription: Practical examples and recipes for spell.\n---\n\n## Examples\n\n- [Validating API Payloads](./examples/api.md)\n- [Form-Safe Parsing](./examples/forms.md)\n- [Async Business Rules](./examples/async.md)\n- [Schema Introspection and Round-Trips](./examples/introspection.md)\n- [Unions, Intersections, and Variants](./examples/unions.md)\n- [Schema Traversal with walk()](./examples/walk.md)\n"
|
|
8
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "array-validation",
|
|
12
|
+
"code": "// Validate a product tag list before it hits search filters.\nimport { s } from '@vielzeug/spell'\n\nconst ProductTags = s.array(s.string().trim().min(2)).min(1).max(4).unique()\n\nconsole.log('Valid tags:', ProductTags.safeParse(['ui', 'forms', 'docs']).success)\n\nconst invalid = ProductTags.safeParse(['ui', 'ui', 'x', 'search', 'extra'])\nconsole.log('Invalid tags:', invalid.success)\n\nif (!invalid.success) {\n console.log('Issues:', invalid.error.issues.map((issue) => issue.message))\n}",
|
|
13
|
+
"name": "Array Validation"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "async-validate",
|
|
17
|
+
"code": "// checkAsync() declares asynchronous domain rules.\n// Use safeParseAsync() or parseAsync() for schemas containing async checks.\nimport { s } from '@vielzeug/spell'\n\n// Simulated async check (e.g. database lookup)\nfunction isUsernameAvailable(name) {\n return new Promise(resolve => setTimeout(() => resolve(name !== 'taken'), 50))\n}\n\nconst UsernameSchema = s.string()\n .min(3)\n .checkAsync(async (name) => {\n const available = await isUsernameAvailable(name)\n return available || 'Username is already taken'\n })\n\n// Async checks require safeParseAsync() or parseAsync()\nconst ok = await UsernameSchema.safeParseAsync('alice')\nconsole.log('alice:', ok.success ? 'available' : ok.error.issues[0].message)\n\nconst fail = await UsernameSchema.safeParseAsync('taken')\nconsole.log('taken:', fail.success ? 'available' : fail.error.issues[0].message)\n\nconst tooShort = await UsernameSchema.safeParseAsync('ab')\nconsole.log('ab:', tooShort.success ? 'available' : tooShort.error.issues[0].message)",
|
|
18
|
+
"name": "Async Validation"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "basic-parsing",
|
|
22
|
+
"code": "// Schema definition, type inference, and safe parsing\nimport { s } from '@vielzeug/spell'\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1).max(120),\n price: s.number().positive().multipleOf(0.01),\n tags: s.array(s.string().min(1)).default(() => []),\n})\n\n// Infer the TypeScript type directly from the schema\n// type Product = { id: string; name: string; price: number; tags: string[] }\n\n// parse() throws on failure — use when invalid input is a programmer error\nconst product = Product.parse({\n id: '550e8400-e29b-41d4-a716-446655440000',\n name: 'Mechanical Keyboard',\n price: 129.99,\n})\nconsole.log('Parsed:', product.name, '— tags:', product.tags)\n\n// safeParse() returns a tagged result union — use at untrusted boundaries\nconst bad = Product.safeParse({ id: 'not-a-uuid', name: '', price: -5 })\nif (!bad.success) {\n const paths = bad.error.issues.map(i => i.path.join('.') || 'root')\n console.log('Validation failed at:', paths.join(', '))\n}",
|
|
23
|
+
"name": "Basic Parsing"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "basic-schema",
|
|
27
|
+
"code": "// Validate a signup payload before it enters application state.\nimport { s } from '@vielzeug/spell'\n\nconst Signup = s.object({\n email: s.string().email(),\n password: s.string().min(12),\n referralCode: s.string().optional(),\n})\n\nconsole.log('Accepted:', Signup.parse({\n email: 'ada@example.com',\n password: 'horse-battery-staple',\n}))\n\nconst invalid = Signup.safeParse({\n email: 'not-an-email',\n password: 'short',\n})\n\nif (!invalid.success) {\n console.log('Email errors:', invalid.error.messagesAt('email'))\n console.log('Password errors:', invalid.error.messagesAt('password'))\n}",
|
|
28
|
+
"name": "Basic Schema Validation"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "coercion",
|
|
32
|
+
"code": "// Coerce query params into typed search options with safe defaults.\nimport { s } from '@vielzeug/spell'\n\nconst SearchQuery = s.object({\n draft: s.coerce.boolean().default(false),\n limit: s.coerce.number().int().positive().default(20),\n page: s.coerce.number().int().positive().default(1),\n q: s.coerce.string().trim().min(1).optional(),\n})\n\nconst parsed = SearchQuery.parse({\n draft: 'true',\n limit: '50',\n page: '2',\n q: ' vielzeug ',\n})\n\nconsole.log(parsed)\nconsole.log('limit type:', typeof parsed.limit)",
|
|
33
|
+
"name": "Type Coercion"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "descriptor-roundtrip",
|
|
37
|
+
"code": "import { s } from '@vielzeug/spell'\nimport { fromDefinition } from '@vielzeug/spell/json'\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1),\n price: s.number().positive(),\n})\n\nconst definition = Product.definition()\nconst jsonSchema = fromDefinition(definition)\n\nconsole.log(definition.kind)\nconsole.log(jsonSchema)",
|
|
38
|
+
"name": "Declarative Definition Export"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "discriminated-union",
|
|
42
|
+
"code": "// s.discriminatedUnion() validates a discriminated union — objects sharing a common tag field.\n// Spell automatically injects the discriminator literal into each branch.\nimport { s } from '@vielzeug/spell'\n\nconst Event = s.discriminatedUnion('type', {\n click: s.object({ x: s.number(), y: s.number() }),\n keydown: s.object({ key: s.string(), repeat: s.boolean() }),\n resize: s.object({ width: s.number(), height: s.number() }),\n})\n\nconst click = Event.parse({ type: 'click', x: 100, y: 200 })\nconsole.log('click:', click)\n\nconst key = Event.parse({ type: 'keydown', key: 'Enter', repeat: false })\nconsole.log('keydown:', key)\n\n// Wrong discriminator value\nconst bad = Event.safeParse({ type: 'unknown', x: 0 })\nconsole.log('unknown type:', bad.success ? 'ok' : bad.error.issues[0].message)\n\n// Missing required field in matched branch\nconst missingField = Event.safeParse({ type: 'resize', width: 800 })\nconsole.log('missing height:', missingField.success ? 'ok' : missingField.error.issues[0].message)",
|
|
43
|
+
"name": "Discriminated Union"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "format-validators",
|
|
47
|
+
"code": "import { s } from '@vielzeug/spell'\nimport { isEmail, isUuid } from '@vielzeug/spell/predicates'\n\nconsole.log(isEmail('ada@example.com'))\nconsole.log(isEmail('not-an-email'))\nconsole.log(isUuid('550e8400-e29b-41d4-a716-446655440000'))\nconsole.log(isUuid('short'))\n\nconst UserId = s.string().uuid()\nconsole.log(UserId.safeParse('550e8400-e29b-41d4-a716-446655440000').success)",
|
|
48
|
+
"name": "Format Predicates"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "messages-override",
|
|
52
|
+
"code": "import { diagnostics, s } from '@vielzeug/spell'\n\nconst context = diagnostics.createParseContext({\n object: { invalidKeys: () => 'Use only supported fields' },\n})\n\nconsole.log(s.object({ email: s.string().email() }).safeParse({ email: 'ada@example.com', extra: true }, context).success)",
|
|
53
|
+
"name": "Request-local messages"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "nested-objects",
|
|
57
|
+
"code": "// Model an API response with a discriminator instead of a loose union.\nimport { s } from '@vielzeug/spell'\n\nconst SearchResponse = s.discriminatedUnion('status', {\n error: s.object({\n message: s.string().min(1),\n status: s.literal('error'),\n }),\n success: s.object({\n results: s.array(s.object({ id: s.string().uuid(), title: s.string().min(1) })).default(() => []),\n status: s.literal('success'),\n }),\n})\n\nconsole.log('Success branch:', SearchResponse.parse({\n status: 'success',\n results: [{ id: '550e8400-e29b-41d4-a716-446655440000', title: 'Spell docs' }],\n}))\n\nconst invalid = SearchResponse.safeParse({ status: 'success', message: 'no results here' })\nconsole.log('Invalid branch accepted:', invalid.success)",
|
|
58
|
+
"name": "Variant Responses"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "number-validation",
|
|
62
|
+
"code": "// Enforce money-like numeric constraints for a checkout amount.\nimport { s } from '@vielzeug/spell'\n\nconst CheckoutTotal = s.number().nonNegative().multipleOf(0.01).max(9999)\n\nfor (const value of [129.99, -4, 19.999, 15000]) {\n const result = CheckoutTotal.safeParse(value)\n console.log(value, '=>', result.success ? 'accepted' : result.error.issues[0].message)\n}",
|
|
63
|
+
"name": "Number Validation"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "object-defaults",
|
|
67
|
+
"code": "import { s } from '@vielzeug/spell';\n\n// Schema where all fields have defaults\nconst ServerConfig = s.object({\n host: s.string().default('localhost'),\n port: s.number().int().positive().default(3000),\n tls: s.boolean().default(false),\n});\n\n// Get a fully filled config without providing any input\nconst config = ServerConfig.defaults();\nconsole.log(config);\n// { host: 'localhost', port: 3000, tls: false }\n\n// Works with nested schemas too\nconst AppConfig = s.object({\n server: ServerConfig,\n debug: s.boolean().default(false),\n});\n\n// Parse with partial input — missing fields use their defaults\nconst parsed = AppConfig.parse({ server: { host: 'prod.example.com', port: 443, tls: true }, debug: true });\nconsole.log(parsed.server.host); // 'prod.example.com'\n\n// Schema with required field (no default) — throws if .defaults() called\nconst Strict = s.object({ name: s.string() });\nconst result = Strict.safeParse({});\nconsole.log(result.success); // false — name is required\n",
|
|
68
|
+
"name": "Object Defaults"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "object-merge",
|
|
72
|
+
"code": "import { s } from '@vielzeug/spell';\n\n// merge() combines two object schemas (right-hand fields win on conflict)\nconst Base = s.object({\n id: s.string().uuid(),\n createdAt: s.date(),\n});\n\nconst WithMeta = s.object({\n description: s.string().optional(),\n tags: s.array(s.string()).default(() => []),\n});\n\nconst Resource = Base.merge(WithMeta);\n\nconst result = Resource.parse({\n createdAt: new Date('2025-01-01'),\n id: '550e8400-e29b-41d4-a716-446655440000',\n tags: ['api', 'v2'],\n});\nconsole.log(result.tags); // ['api', 'v2']\nconsole.log(result.id); // '550e8400-...'\n\n// merge() inherits the right-hand schema's strict/relaxed mode\nconst Strict = s.object({ a: s.string() });\nconst Relaxed = s.object({ b: s.number() }).relaxed();\n\nconst Merged = Strict.merge(Relaxed);\n// Extra keys are allowed because Relaxed is the right-hand schema\nconsole.log(Merged.safeParse({ a: 'hi', b: 1, extra: true }).success); // true\n\nconst IdOrSlug = s.union(s.string().uuid(), s.string().slug());\nconsole.log(IdOrSlug.safeParse('550e8400-e29b-41d4-a716-446655440000').success); // true\nconsole.log(IdOrSlug.safeParse('my-slug').success); // true\nconsole.log(IdOrSlug.safeParse(42).success); // false\n\nconst NonEmptyString = s.intersect(s.string(), s.string().min(1));\nconsole.log(NonEmptyString.parse('hello')); // 'hello'\n",
|
|
73
|
+
"name": "Object Merge & Aliases"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "optional-nullable",
|
|
77
|
+
"code": "// Preserve defaults and validators while tightening undefined away with required().\nimport { s } from '@vielzeug/spell'\n\nconst DisplayName = s.string().trim().min(2).optional().default('Guest').nullable()\nconst RequiredDisplayName = DisplayName.required()\n\nconsole.log('default for undefined:', DisplayName.parse(undefined))\nconsole.log('null stays null:', DisplayName.parse(null))\n\nconst short = RequiredDisplayName.safeParse('A')\nconsole.log('short name accepted:', short.success)\n\nconst missing = RequiredDisplayName.safeParse(undefined)\nconsole.log('undefined accepted after required():', missing.success)\n\nconsole.log('null accepted after required():', RequiredDisplayName.parse(null))",
|
|
78
|
+
"name": "Optional and Nullable Fields"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "refinements",
|
|
82
|
+
"code": "// check() and checkAsync() — explicit custom domain rules\nimport { s } from '@vielzeug/spell'\n\nconst reserved = new Set(['admin', 'root'])\n\n// check() is synchronous; return a string to fail with that message\nconst Username = s.string().min(3).check((value) =>\n !reserved.has(value) || value + ' is reserved'\n)\n\n// check() receives context for multiple issues or custom error codes\nconst Signup = s.object({ password: s.string().min(8), confirm: s.string() })\n .check((v, ctx) => {\n if (v.password !== v.confirm)\n ctx.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] })\n })\n\n// check() also covers predicate-only domain rules\nconst EvenPort = s.number().int().min(1).max(65535)\n .check((n) => n % 2 === 0 || 'Port must be even')\n\nfor (const name of ['ad', 'admin', 'grace']) {\n const r = Username.safeParse(name)\n console.log(name, '->', r.success ? 'ok' : r.error.issues[0].message)\n}\n\nconst signupResult = Signup.safeParse({ password: 'secure123', confirm: 'different' })\nconsole.log('signup:', signupResult.success ? 'ok' : signupResult.error.issues[0].message)\n\nfor (const port of [8080, 3001, 443]) {\n const r = EvenPort.safeParse(port)\n console.log('port', port, '->', r.success ? 'ok' : r.error.issues[0].message)\n}",
|
|
83
|
+
"name": "Custom Validation"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "schema-walk",
|
|
87
|
+
"code": "// Traverse a schema tree with walk() to extract field metadata.\nimport { s } from '@vielzeug/spell'\n\nconst Order = s.object({\n id: s.string().uuid(),\n amount: s.number().positive(),\n customer: s.object({\n email: s.string().email(),\n name: s.string().min(1),\n }),\n tags: s.array(s.string()).optional(),\n})\n\n// Collect every field name and whether it is optional.\nconst fields: { name: string; required: boolean }[] = []\n\nOrder.walk({\n object(node) {\n for (const [key, child] of Object.entries(node.shape)) {\n fields.push({ name: key, required: !child.isOptional })\n child.walk(this)\n }\n },\n // unknown() catches any kind without a handler; omitting it returns null instead of throwing\n unknown() {},\n})\n\nconsole.log('Fields:')\nfields.forEach(f => console.log(' ', f.name, f.required ? '(required)' : '(optional)'))\nconsole.log('Total:', fields.length)",
|
|
88
|
+
"name": "Schema Traversal"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "string-validation",
|
|
92
|
+
"code": "// Reuse one stateful regex safely across repeated parses in the browser REPL.\nimport { s } from '@vielzeug/spell'\n\nconst HexColor = s.string().regex(/#[0-9a-f]{6}/gy)\n\nfor (const value of ['#ff8800', '#ff8800', 'oops']) {\n const result = HexColor.safeParse(value)\n console.log(value, '=>', result.success)\n}",
|
|
93
|
+
"name": "String Validation"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "wrappers-and-defaults",
|
|
97
|
+
"code": "// optional(), nullable(), default(), catch() — missing-value semantics\nimport { s } from '@vielzeug/spell'\n\n// optional: accepts undefined, passes through validation otherwise\nconst Nickname = s.string().min(2).optional().default('Guest')\n\nconsole.log(Nickname.parse(undefined)) // 'Guest'\nconsole.log(Nickname.parse('Ada')) // 'Ada'\n\n// nullable: accepts null explicitly\nconst Bio = s.string().max(200).nullable()\n\nconsole.log(Bio.parse(null)) // null\nconsole.log(Bio.parse('Loves types')) // 'Loves types'\n\n// nullish: accepts both null and undefined\nconst Avatar = s.string().url().nullish()\n\nconsole.log(Avatar.parse(null)) // null\nconsole.log(Avatar.parse(undefined)) // undefined\n\n// required(): strips undefined without removing null\nconst NullableButRequired = s.string().optional().nullable().required()\nconsole.log(NullableButRequired.parse(null)) // null\nconsole.log(NullableButRequired.safeParse(undefined).success) // false\n\n// catch(): returns a fallback when validation fails — never throws\nconst Port = s.number().int().min(1).max(65535).catch(3000)\nconsole.log(Port.parse(8080)) // 8080\nconsole.log(Port.parse('not-a-port')) // 3000",
|
|
98
|
+
"name": "Wrappers & Defaults"
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"typeSignatures": {
|
|
102
|
+
"AnySchema": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
103
|
+
"CheckContext": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
104
|
+
"FlatError": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
105
|
+
"FlatErrorFirst": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
106
|
+
"Infer": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
107
|
+
"InferInput": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
108
|
+
"InferOutput": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
109
|
+
"InferSchemaMode": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
110
|
+
"Issue": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
111
|
+
"JsonSchema": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
112
|
+
"MergeSchemaModes": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
113
|
+
"MessageFn": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
114
|
+
"Messages": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
115
|
+
"ParseContext": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
116
|
+
"ParseResult": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
117
|
+
"SchemaDescriptor": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
118
|
+
"SchemaMode": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
119
|
+
"SchemaWalker": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
120
|
+
"ValidateFn": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
121
|
+
"ValidateResult": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';",
|
|
122
|
+
"ErrorCode": "export {\n ErrorCode,\n PipeSchema,\n Schema,\n SpellDefinitionError,\n SpellError,\n SpellValidationError,\n schemaMode,\n} from './core';",
|
|
123
|
+
"PipeSchema": "export {\n ErrorCode,\n PipeSchema,\n Schema,\n SpellDefinitionError,\n SpellError,\n SpellValidationError,\n schemaMode,\n} from './core';",
|
|
124
|
+
"Schema": "export {\n ErrorCode,\n PipeSchema,\n Schema,\n SpellDefinitionError,\n SpellError,\n SpellValidationError,\n schemaMode,\n} from './core';",
|
|
125
|
+
"SpellDefinitionError": "export {\n ErrorCode,\n PipeSchema,\n Schema,\n SpellDefinitionError,\n SpellError,\n SpellValidationError,\n schemaMode,\n} from './core';",
|
|
126
|
+
"SpellError": "export {\n ErrorCode,\n PipeSchema,\n Schema,\n SpellDefinitionError,\n SpellError,\n SpellValidationError,\n schemaMode,\n} from './core';",
|
|
127
|
+
"SpellValidationError": "export {\n ErrorCode,\n PipeSchema,\n Schema,\n SpellDefinitionError,\n SpellError,\n SpellValidationError,\n schemaMode,\n} from './core';",
|
|
128
|
+
"schemaMode": "export {\n ErrorCode,\n PipeSchema,\n Schema,\n SpellDefinitionError,\n SpellError,\n SpellValidationError,\n schemaMode,\n} from './core';",
|
|
129
|
+
"DeepPartial": "export type { DeepPartial } from './messages';",
|
|
130
|
+
"s": "export { s } from './s';",
|
|
131
|
+
"diagnostics": "export const diagnostics = {\n createParseContext,\n fail,\n prependIssuePath,\n};"
|
|
132
|
+
}
|
|
133
|
+
}
|