@vielzeug/codex 3.0.2 → 3.1.0

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.
Files changed (53) hide show
  1. package/README.md +6 -0
  2. package/data/catalog.json +1984 -0
  3. package/data/llms-full.txt +31429 -0
  4. package/data/llms.txt +47 -0
  5. package/data/manifest.json +8 -0
  6. package/data/packages/arsenal.json +216 -0
  7. package/data/packages/assay.json +46 -0
  8. package/data/packages/clockwork.json +66 -0
  9. package/data/packages/codex.json +29 -0
  10. package/data/packages/coins.json +101 -0
  11. package/data/packages/conduit.json +68 -0
  12. package/data/packages/courier.json +55 -0
  13. package/data/packages/dnd.json +60 -0
  14. package/data/packages/familiar.json +17 -0
  15. package/data/packages/flux.json +94 -0
  16. package/data/packages/focus.json +38 -0
  17. package/data/packages/forge.json +60 -0
  18. package/data/packages/gesture.json +37 -0
  19. package/data/packages/herald.json +81 -0
  20. package/data/packages/illusionist.json +132 -0
  21. package/data/packages/keymap.json +51 -0
  22. package/data/packages/ledger.json +60 -0
  23. package/data/packages/lingua.json +70 -0
  24. package/data/packages/mesh.json +54 -0
  25. package/data/packages/necromancer.json +50 -0
  26. package/data/packages/orbit.json +99 -0
  27. package/data/packages/ore.json +65 -0
  28. package/data/packages/postmaster.json +49 -0
  29. package/data/packages/prism.json +66 -0
  30. package/data/packages/pulse.json +71 -0
  31. package/data/packages/refine.json +12 -0
  32. package/data/packages/ripple.json +88 -0
  33. package/data/packages/rune.json +78 -0
  34. package/data/packages/sandbox.json +26 -0
  35. package/data/packages/scout.json +63 -0
  36. package/data/packages/scroll.json +110 -0
  37. package/data/packages/sentinel.json +35 -0
  38. package/data/packages/sigil.json +49 -0
  39. package/data/packages/sourcerer.json +70 -0
  40. package/data/packages/spell.json +132 -0
  41. package/data/packages/tempo.json +81 -0
  42. package/data/packages/vault.json +93 -0
  43. package/data/packages/ward.json +80 -0
  44. package/data/packages/wayfinder.json +114 -0
  45. package/data/refine.json +12828 -0
  46. package/data/search.json +1582 -0
  47. package/dist/cli.js +32 -0
  48. package/dist/cli.js.map +1 -1
  49. package/dist/skills.d.ts +26 -0
  50. package/dist/skills.js +46 -0
  51. package/dist/skills.js.map +1 -0
  52. package/package.json +6 -6
  53. package/skills/vielzeug/SKILL.md +153 -0
@@ -0,0 +1,88 @@
1
+ {
2
+ "apiSource": "export type { AsyncState, Resource, ResourceOptions } from './_async';\nexport { createRipple, type Ripple } from './_default';\nexport type { BridgedReadable, FromSubscribableOptions } from './_subscribable';\nexport type { WatchOptions } from './_watch';\nexport {\n RippleComputedCycleError,\n RippleDisposedResourceError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';\nexport { isReactive } from './runtime';\nexport type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';\n\nimport { defaultRipple } from './_default';\n\nexport const signal = defaultRipple.signal;\nexport const computed = defaultRipple.computed;\nexport const effect = defaultRipple.effect;\nexport const batch = defaultRipple.batch;\nexport const createScope = defaultRipple.createScope;\nexport const untrack = defaultRipple.untrack;\nexport const watch = defaultRipple.watch;\nexport const resource = defaultRipple.resource;\nexport const fromSubscribable = defaultRipple.fromSubscribable;\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Ripple — Reactive graphs\ndescription: Framework-agnostic signals, effects, async resources, scopes, structural bridges, and observability.\npackage: ripple\ncategory: state\nkeywords: [reactive, signals, computed, effects, graph, scope, batch, watch, tap, observability]\nrelated: [ore, clockwork, ledger]\nexports: [createRipple, signal, computed, effect, batch, createScope, untrack, watch, resource, fromSubscribable, isReactive]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ripple\" />\n\n## Why Ripple?\n\nHand-rolled reactive state spreads subscription, cleanup, and derived-value rules across application code. Ripple gives you one graph boundary with explicit disposal and fine-grained dependencies while keeping rendering and routing outside the runtime.\n\n```ts\n// Before\nlet count = 0;\nconst listeners = new Set<() => void>();\n\nfunction setCount(next: number) {\n count = next;\n for (const listener of listeners) listener();\n}\n\n// After\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\ncount.value = 1;\nstop.dispose();\nripple.dispose();\n```\n\n| Feature | Ripple | Zustand | Jotai |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"ripple\" type=\"size\" /> | ~3.5 kB | ~7 kB |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Framework-agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | React-first |\n| Explicit graph lifetime | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Fine-grained derived values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Selectors | Atoms |\n\n<div class=\"decision-callout\">\n\n**Use Ripple when** you need framework-independent state with explicit graph lifetime and small composable primitives.\n\n**Consider a framework store when** component bindings, server cache, or framework-specific tooling matter more than portable reactive state.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\nCreate one graph, derive a value, observe it, then dispose when the graph lifetime ends.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\nripple.batch(() => {\n count.value = 1;\n count.value = 2;\n});\n\nstop.dispose();\nripple.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createRipple()` creates an isolated graph and lifetime boundary.\n- `signal()` stores writable values with configurable equality.\n- `computed()` derives lazy read-only values.\n- `effect()` reacts to dependency changes with cleanup support.\n- `batch()` coalesces synchronous writes and notifications.\n- `createScope()` groups owned reactive work.\n- `watch()` observes one selected source transition.\n- `tap()` provides runtime observability for all graph events.\n- `resource()` owns focused latest-request async state.\n- `fromSubscribable()` creates a disposable bridge for external `{ getSnapshot, subscribe }` sources.\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- [Ore](/ore/) — uses Ripple signals and effects for web-component reactivity.\n- [Clockwork](/clockwork/) — exposes machine state through reactive Ripple values.\n- [Ledger](/ledger/) — adds command-based undo and redo beside Ripple state.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
5
+ "api": "---\ntitle: Ripple — API Reference\ndescription: Complete reference for reactive graphs, signals, effects, scopes, watchers, and observability.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createRipple()` | Create isolated graph | Sync | Disposal is terminal; create a new graph instead of reusing it |\n| `signal()` | Create writable value | Sync | Default graph is process-wide |\n| `computed()` | Create lazy derived value | Sync | Keep derivation pure |\n| `effect()` | React to dependency reads | Sync | Dispose handle or return cleanup |\n| `batch()` | Coalesce synchronous writes | Sync | Does not roll back writes |\n| `createScope()` | Group owned reactive work | Sync | Call `run()` to activate it |\n| `untrack()` | Read without tracking | Sync | Read still happens immediately |\n| `watch()` | Observe selected output | Sync | Use `effect()` for broad reads |\n| `tap()` | Observe runtime events | Sync | Handler errors are swallowed |\n| `fromSubscribable()` | Bridge external source | Sync | Returned bridge must be disposed |\n| `isReactive()` | Test Ripple's marked readable contract | Sync | Not a security or provenance check |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ripple` | All primitives, types, and errors — signals, computed, effects, scopes, watch, tap, and the isolated graph factory |\n\n## Graph Creation\n\n### `createRipple(options?)`\n\n```ts\nfunction createRipple(options?: RippleOptions): Ripple;\n```\n\nCreates one isolated reactive graph. Factories on the returned object share scheduling, ownership, and error boundaries. `dispose()` is terminal: `ripple.disposed` becomes `true`, existing owned work is disposed, and creating more graph work throws `RippleDisposedRuntimeError`. Create a new graph for a new lifetime.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.errorPolicy` | `'throw' \\| 'swallow'` | Controls whether computed refresh, effect, cleanup, and listener failures rethrow. Default `'throw'`. Error events are always emitted through `tap()`. |\n\n**Returns:** `Ripple`.\n\n**Example:**\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n---\n\n### `isReactive(value)`\n\n```ts\nfunction isReactive<T>(value: T | Readable<T>): value is Readable<T>;\n```\n\nTests Ripple's shared-symbol marker and complete readable surface (`value`, `peek()`, and `subscribe()`). Recognition works across duplicated Ripple module graphs. The marker is intentionally structural and forgeable; do not use this guard for trust or provenance decisions.\n\n**Returns:** `true` for marked signals, computeds, resources, and external-source bridges with the complete readable contract.\n\n**Example:**\n\n```ts\nimport { isReactive, signal } from '@vielzeug/ripple';\n\nconsole.log(isReactive(signal(0)));\n```\n\n## Default Graph Functions\n\nRoot helpers (`signal`, `computed`, `effect`, `batch`, `createScope`, `untrack`, `watch`, `resource`, `fromSubscribable`) operate on a process-lifetime default graph. Use them only when the application has one graph for its entire lifetime — they are never disposed. For isolated, disposable graphs (tests, SSR, embedded features) use `createRipple()`.\n\n### `signal(initial, options?)`\n\n```ts\nfunction signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n```\n\nCreates writable state on the default graph. Use `update()` for immutable replacement patterns.\n\n**Returns:** `Signal<T>`.\n\n**Example:**\n\n```ts\nimport { signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\ncount.value += 1;\n\nconst cart = signal({ items: 0 });\ncart.update((state) => ({ ...state, items: state.items + 1 }));\n```\n\n---\n\n### `computed(derive, options?)`\n\n```ts\nfunction computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n```\n\nCreates a lazy read-only value from reactive reads in `derive`. `peek()` may trigger derivation and throw directly. During dependency propagation, a computed refresh failure is reported through `tap()` with context kind `computed` and does not prevent sibling dependents from updating. A disposed computed returns its last cached value from `peek()` without recomputing.\n\n**Returns:** `Readable<T>`.\n\n**Example:**\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\n\nconst count = signal(2);\nconst doubled = computed(() => count.value * 2);\nconsole.log(doubled.value);\n```\n\n---\n\n### `effect(callback, options?)`\n\n```ts\nfunction effect(callback: () => Cleanup | undefined, options?: EffectOptions): EffectHandle;\n```\n\nRuns immediately and reruns when its tracked reads change. A returned cleanup runs before the next callback or disposal.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { effect, signal } from '@vielzeug/ripple';\n\nconst connected = signal(false);\nconst stop = effect(() => {\n if (!connected.value) return;\n\n return () => console.log('disconnect');\n});\n\nstop.dispose();\n```\n\n---\n\n### `batch(fn)` and `untrack(fn)`\n\n```ts\nfunction batch<T>(fn: () => T): T;\nfunction untrack<T>(fn: () => T): T;\n```\n\n`batch()` defers effects and listeners until its callback returns. `untrack()` reads current state without adding dependencies to an enclosing effect.\n\n**Returns:** the callback result.\n\n**Example:**\n\n```ts\nimport { batch, signal, untrack } from '@vielzeug/ripple';\n\nconst first = signal('Ada');\nconst last = signal('Lovelace');\nconst locale = signal('en-US');\n\nbatch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n\nconsole.log(untrack(() => locale.value));\n```\n\n---\n\n### `createScope(name?)`\n\n```ts\nfunction createScope(name?: string): Scope;\n```\n\nCreates a disposable ownership boundary. Work created inside `scope.run()` belongs to that scope. Explicit scopes created during an effect run attach to the enclosing scope—not the transient effect-run owner—and therefore survive reruns until explicitly or parent-disposed. Disposed children detach from parent ownership immediately.\n\n**Returns:** `Scope`.\n\n**Example:**\n\n```ts\nimport { createScope, effect, signal } from '@vielzeug/ripple';\n\nconst scope = createScope('panel');\nconst count = signal(0);\n\nscope.run(() => effect(() => console.log(count.value)));\nscope.dispose();\n```\n\n### `resource(source, loader, options?)`\n\n```ts\nfunction resource<Source, Value>(\n source: () => Source,\n loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>,\n options?: ResourceOptions,\n): Resource<Value>;\n```\n\nCreates a disposable readable async state and starts loading immediately. Source changes and `reload()` abort stale work before starting the next loader. The last successful value is preserved as `previous`, including when that value is `undefined`.\n\n```ts\nconst user = resource(\n () => userId.value,\n (id, { signal }) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),\n);\n```\n\nThe state is `{ status: 'pending', previous? }`, `{ status: 'success', value }`, or `{ status: 'error', error, previous? }`. Reads remain available after disposal; `reload()` and new subscriptions throw `RippleDisposedResourceError`.\n\n## Watch and Observability\n\n### `watch(source, callback, options?)`\n\n```ts\nfunction watch<T>(\n source: Readable<T> | (() => T),\n callback: (value: T, previous: T | undefined) => void,\n options?: WatchOptions<T>,\n): EffectHandle;\n```\n\nObserves selected output changes using the default graph or a `Ripple.watch()` method. Callback-only reactive reads are untracked. `once` disposes after the first callback invocation even when it throws; without `immediate`, initial evaluation does not count as an invocation.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst stop = watch(count, (value, previous) => console.log(previous, value), { immediate: true });\nstop.dispose();\n```\n\n---\n\n### `ripple.tap(handler, options?)`\n\n```ts\nfunction tap(\n handler: (event: RippleEvent) => void,\n options?: { signal?: AbortSignal },\n): Unsubscribe;\n```\n\nRuntime observability for writes, computes, effects, disposals, and reported errors. Computed refresh errors are reported with context kind `computed` without interrupting sibling propagation. During graph disposal, taps remain active for owned cleanup errors and node disposal events; `dispose:graph` is emitted last. Handler errors are swallowed. The returned function unsubscribes, and an optional signal auto-detaches it.\n\nThe `Ripple.tap()` method is available on instances created with `createRipple()`. After runtime disposal it returns a no-op unsubscribe.\n\n**Returns:** `Unsubscribe`.\n\n**Example:**\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple({ errorPolicy: 'swallow' });\nconst stop = ripple.tap((event) => {\n if (event.type === 'error') console.error(event.context.kind, event.error);\n if (event.type === 'write') console.log(`${event.name}: ${event.previous} → ${event.next}`);\n});\n\nconst count = ripple.signal(0);\ncount.value = 1;\n\nstop();\nripple.dispose();\n```\n\n---\n\n### `fromSubscribable(source, options?)`\n\n```ts\nfunction fromSubscribable<T>(source: Subscribable<T>, options?: FromSubscribableOptions): BridgedReadable<T>;\n```\n\nBridges an external `{ getSnapshot, subscribe }` source into a disposable reactive readable. The bridge subscribes and then rereads the snapshot to avoid missing a change during registration. The root helper uses the default graph; `ripple.fromSubscribable()` binds ownership to an isolated graph. An optional signal disposes the bridge.\n\n**Returns:** `BridgedReadable<T>`. Disposal invokes the source unsubscribe function. Reads remain available afterward, while new subscriptions throw.\n\n**Example:**\n\n```ts\nimport { fromSubscribable } from '@vielzeug/ripple';\n\nconst routerState = fromSubscribable({\n getSnapshot: () => router.getSnapshot(),\n subscribe: (cb) => router.subscribe(cb),\n});\n\nrouterState.dispose();\n```\n\n## Types\n\n```ts\ntype Cleanup = () => void;\ntype Equality<T> = (previous: T, next: T) => boolean;\ntype Unsubscribe = () => void;\n\ntype SignalOptions<T> = { equals?: Equality<T>; name?: string };\ntype ComputedOptions<T> = { equals?: Equality<T>; name?: string };\ntype EffectOptions = { name?: string; scheduler?: 'microtask' | 'sync' };\ntype WatchOptions<T> = { equals?: Equality<T>; immediate?: boolean; name?: string; once?: boolean };\n\ntype RippleErrorContext = { readonly kind: 'cleanup' | 'computed' | 'effect' | 'listener'; readonly name?: string };\ntype RippleErrorPolicy = 'throw' | 'swallow';\ntype RippleOptions = { errorPolicy?: RippleErrorPolicy };\n\ntype RippleEvent =\n | { readonly type: 'compute'; readonly name?: string }\n | { readonly type: 'effect'; readonly name?: string }\n | { readonly type: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }\n | { readonly type: 'dispose'; readonly name?: string; readonly node: 'effect' | 'graph' | 'scope' }\n | { readonly type: 'error'; readonly error: unknown; readonly context: RippleErrorContext };\n\ninterface Readable<T> {\n readonly name?: string;\n peek(): T;\n subscribe(listener: () => void): Unsubscribe;\n readonly value: T;\n}\n\ninterface Signal<T> extends Readable<T> { update(updater: (prev: T) => T): void; value: T }\ninterface Disposable { dispose(): void; readonly disposed: boolean; readonly disposalSignal: AbortSignal; [Symbol.dispose](): void }\ntype EffectHandle = Disposable;\ninterface Scope extends Disposable { run<T>(fn: () => T): T }\n\ninterface Subscribable<T> {\n getSnapshot(): T;\n subscribe(listener: () => void): Unsubscribe;\n}\n\ntype FromSubscribableOptions = { name?: string; signal?: AbortSignal };\ntype BridgedReadable<T> = Readable<T> & Disposable;\n\ntype AsyncState<T> =\n | { readonly previous?: T; readonly status: 'pending' }\n | { readonly status: 'success'; readonly value: T }\n | { readonly error: unknown; readonly previous?: T; readonly status: 'error' };\n\ninterface Resource<T> extends Readable<AsyncState<T>>, Disposable {\n reload(): void;\n}\n\ntype ResourceOptions = { name?: string };\n\ninterface Ripple extends Disposable {\n batch<T>(fn: () => T): T;\n computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n createScope(name?: string): Scope;\n effect(callback: () => Cleanup | undefined, options?: EffectOptions): EffectHandle;\n fromSubscribable<T>(source: Subscribable<T>, options?: FromSubscribableOptions): BridgedReadable<T>;\n resource<Source, Value>(\n source: () => Source,\n loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>,\n options?: ResourceOptions,\n ): Resource<Value>;\n signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n tap(handler: (event: RippleEvent) => void, options?: { signal?: AbortSignal }): Unsubscribe;\n untrack<T>(fn: () => T): T;\n watch<T>(source: Readable<T> | (() => T), callback: (value: T, previous: T | undefined) => void, options?: WatchOptions<T>): EffectHandle;\n}\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `RippleError` | Base Ripple error | Use `instanceof RippleError` to narrow unknown values. |\n| `RippleComputedCycleError` | Computed dependency reads itself through a cycle | Extends `RippleError`. |\n| `RippleDisposedResourceError` | `reload()` or `subscribe()` used after resource disposal | Extends `RippleError`. |\n| `RippleDisposedRuntimeError` | Factory or execution API used after `ripple.dispose()` | Extends `RippleError`. |\n| `RippleDisposedScopeError` | `scope.run()` after scope disposal | Extends `RippleError`. |\n| `RippleInfiniteLoopError` | Effect flush exceeds graph iteration limit | Extends `RippleError`. |\n",
6
+ "usage": "---\ntitle: Ripple — Usage Guide\ndescription: Build reactive state with one explicit graph boundary.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse top-level functions when one application-lifetime graph is sufficient. Read a signal inside an effect to make that read reactive.\n\n```ts\nimport { computed, effect, signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst label = computed(() => `Count: ${count.value}`);\nconst stop = effect(() => console.log(label.value));\n\ncount.value = 1;\nstop.dispose();\n```\n\n## Isolated Graphs\n\nUse `createRipple()` for tests, SSR requests, embedded applications, or independently disposable features. Never mix reactive values from separate graphs.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple({ errorPolicy: 'swallow' });\n\nripple.tap((event) => {\n if (event.type === 'error') console.log(event.context.kind, event.error);\n});\n\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n## Derived Values and Batches\n\nUse `computed()` for pure derivation. Use `untrack()` when a current read must not become an effect dependency. Use `batch()` for related synchronous writes.\n\n```ts\nconst first = ripple.signal('Ada');\nconst last = ripple.signal('Lovelace');\nconst locale = ripple.signal('en-US');\nconst name = ripple.computed(() => `${first.value} ${last.value}`);\n\nripple.effect(() => {\n console.log({ locale: ripple.untrack(() => locale.value), name: name.value });\n});\n\nripple.batch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n```\n\n## Scheduling and Subscriptions\n\nRipple propagates every synchronous write before flushing effects. Each flush pass runs effects queued at its\nstart before direct `subscribe()` listeners queued at its start. Work queued by either runs in a later pass.\nEffects using `scheduler: 'microtask'` join a later microtask and coalesce writes made before that task runs.\n\n```ts\nconst count = ripple.signal(0);\nconst log: string[] = [];\n\nripple.effect(() => log.push(`effect: ${count.value}`));\ncount.subscribe(() => log.push(`listener: ${count.value}`));\nripple.effect(() => log.push(`deferred: ${count.value}`), { scheduler: 'microtask' });\n\nlog.length = 0; // Ignore synchronous creation runs.\ncount.value = 1;\nconsole.log(log); // ['effect: 1', 'listener: 1']\n\nawait Promise.resolve();\nconsole.log(log); // ['effect: 1', 'listener: 1', 'deferred: 1']\n```\n\n## Ownership with Scopes\n\nCreate a scope when a group of effects or derived values shares one lifetime. Dispose the scope when its feature ends.\n\n```ts\nconst scope = ripple.createScope('panel');\nconst count = ripple.signal(0);\n\nscope.run(() => {\n ripple.effect(() => console.log(`Panel count: ${count.value}`));\n});\n\ncount.value = 1;\nscope.dispose();\n```\n\nEffects and computeds created directly during an effect run belong to that run and are disposed before rerun. Explicit scopes intentionally attach to the enclosing scope and survive effect reruns; create them outside rerunning effects or retain and dispose each scope yourself.\n\nDisposed owned nodes detach from their parent immediately, so long-lived graphs do not retain released effects or scopes.\n\n## Watch Selected Values\n\nUse `watch()` for one selected output. Reactive reads made only inside its callback are untracked. `{ once: true }` disposes after the first callback invocation even when that callback throws. Use `effect()` when every callback read should be a dependency.\n\n```ts\nconst stopWatch = ripple.watch(\n () => `${first.value} ${last.value}`,\n (value, previous) => console.log({ previous, value }),\n { immediate: true },\n);\n\nstopWatch.dispose();\n```\n\n## Observability\n\n`tap()` receives writes, computes, effects, disposals, and reported errors. Computed refresh failures are isolated and reported without interrupting sibling propagation. During runtime disposal, owned cleanup errors and node disposal events are emitted before the final graph disposal event; handlers remain active until that sequence completes. Tap-handler errors are swallowed.\n\n```ts\nconst ripple = createRipple({ errorPolicy: 'swallow' });\n\nripple.tap((event) => {\n switch (event.type) {\n case 'write':\n console.log(`${event.name}: ${event.previous} → ${event.next}`);\n break;\n case 'error':\n console.error(`${event.context.kind}:`, event.error);\n break;\n }\n});\n```\n\n`errorPolicy` controls whether computed refresh, effect, cleanup, and listener failures rethrow (`'throw'`, default) or are silenced (`'swallow'`). Error events are emitted through `tap()` regardless of policy.\n\n## Bridging External Sources\n\n`fromSubscribable()` bridges external `{ getSnapshot, subscribe }` sources into a disposable reactive readable. Use `ripple.fromSubscribable()` inside isolated graphs; the root helper uses the process-lifetime default graph. The bridge rereads after subscription to close registration races, and disposal invokes the external unsubscribe function.\n\n```ts\nimport { fromSubscribable, effect } from '@vielzeug/ripple';\n\nconst routerState = fromSubscribable({\n getSnapshot: () => router.getSnapshot(),\n subscribe: (cb) => router.subscribe(cb),\n});\n\nconst stop = effect(() => {\n console.log('Current route:', routerState.value);\n});\n\nstop.dispose();\nrouterState.dispose();\n```\n\n## Load Async Resources\n\n`resource()` starts immediately, tracks its source reads, and aborts the previous loader whenever the source changes or `reload()` is called. It preserves the last successful value—including `undefined`—as `previous` while newer work is pending or fails.\n\n```ts\nconst userId = ripple.signal('42');\nconst user = ripple.resource(\n () => userId.value,\n (id, { signal }) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),\n { name: 'user' },\n);\n\nconst stop = ripple.effect(() => {\n const state = user.value;\n if (state.status === 'success') renderUser(state.value);\n if (state.status === 'error') renderError(state.error);\n});\n\nstop.dispose();\nuser.dispose();\n```\n\nDisposed resources retain readable state but reject `reload()` and new subscriptions with `RippleDisposedResourceError`. Use Sourcerer for paginated collection state and a dedicated server-state cache for shared keyed reads, retries, and invalidation.\n\n## Object State\n\n`signal()` with `update()` holds one value and supports immutable replacement patterns. Return replacement objects from `update()` when object consumers depend on immutable updates.\n\n```ts\nconst cart = ripple.signal({ items: 0, label: 'empty' });\nconst items = ripple.computed(() => cart.value.items);\n\ncart.update((state) => ({ ...state, items: state.items + 1 }));\ncart.value = { items: 3, label: 'ready' };\n\nconsole.log(items.value);\n```\n\n## Testing\n\nCreate an isolated graph per test. Disposal prevents effects from leaking into later tests.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createRipple } from '@vielzeug/ripple';\n\ntest('derives a doubled count', () => {\n const ripple = createRipple();\n const count = ripple.signal(2);\n const doubled = ripple.computed(() => count.value * 2);\n\n expect(doubled.value).toBe(4);\n ripple.dispose();\n});\n```\n\n## Framework Integration\n\nUse signals and effects with any renderer. Dispose component-owned effects when the component unmounts.\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\n\nexport function Counter() {\n const [, rerender] = useState(0);\n\n useEffect(() => {\n const stop = ripple.effect(() => {\n void count.value;\n rerender((revision) => revision + 1);\n });\n\n return () => stop.dispose();\n }, []);\n\n return <button onClick={() => (count.value += 1)}>{count.value}</button>;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst revision = ref(0);\nconst stop = ripple.effect(() => {\n void count.value;\n revision.value++;\n});\n\nonUnmounted(() => stop.dispose());\n```\n\n```ts [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createRipple } from '@vielzeug/ripple';\n\n const ripple = createRipple();\n const count = ripple.signal(0);\n let revision = 0;\n const stop = ripple.effect(() => {\n void count.value;\n revision++;\n });\n\n onDestroy(() => stop.dispose());\n</script>\n\n<button on:click={() => (count.value += 1)}>{count.value}</button>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nOre uses Ripple for component reactivity. Clockwork actors expose framework-neutral snapshots; bridge actor subscriptions into a Ripple signal. Ledger adds undo/redo commands around state changes without replacing graph.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\nimport { defineMachine } from '@vielzeug/clockwork';\n\nconst ripple = createRipple();\nconst actor = defineMachine<Record<string, never>, { type: 'START' }>()({\n initial: 'idle',\n states: { active: {}, idle: { on: { START: { target: 'active' } } } },\n}).createActor();\n\nconst snapshot = ripple.signal(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nconst status = ripple.computed(() => snapshot.value.state);\nconsole.log(status.value);\n\nstop();\nactor.dispose();\nripple.dispose();\n```\n\n## Gotchas\n\n### `subscribe()` forces computed evaluation\n\n`Readable.subscribe()` calls `peek()` before registering the listener. For signals this is a no-op, but for computeds it forces `refresh()` — the derivation runs immediately even if no one reads `.value`. This ensures `equals` comparison works on the first dependency change. Avoid subscribing to expensive computeds unless you need their value.\n\n### Computed first-run failure is recoverable\n\nIf a computed's `derive` throws on its first run (e.g., a source is `null`), the computed commits the partial dependencies it tracked before the throw. When a dependency changes and the derivation can succeed, the computed refreshes and notifies its dependents. Effects that read a failing computed report the error through `tap()` and re-run when the computed recovers.\n\n## Best Practices\n\n- Create one graph per ownership boundary.\n- Keep computed callbacks pure.\n- Return cleanup from effects.\n- Dispose test and feature graphs.\n- Batch related synchronous writes.\n- Use `watch()` only for selected source transitions; callback-only reads are untracked.\n- Dispose external-source bridges when their owner ends.\n- Use `tap()` for runtime observability; set `errorPolicy: 'swallow'` to silence rethrow.\n- Use `resource()` for focused async state and Sourcerer for paginated collection workflows.\n",
7
+ "examples": "---\ntitle: Ripple — Examples\ndescription: Practical Ripple recipes.\n---\n\n## Examples\n\n- [Reactive Counter](./examples/reactive-counter.md)\n- [Batch and Untrack](./examples/batch-and-untrack.md)\n- [Scope Ownership](./examples/scope-ownership.md)\n- [Watch Selected Value](./examples/watch-selected-value.md)\n- [Async Resource](./examples/async-resource.md)\n- [Immutable State](./examples/immutable-store.md)\n- [Isolated Graph](./examples/isolated-runtime.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "async-resource",
12
+ "code": "import { createRipple } from '@vielzeug/ripple'\n\n// Resource reloads from tracked input and ignores stale loader work.\nconst ripple = createRipple()\nconst userId = ripple.signal('u1')\nconst user = ripple.resource(\n () => userId.value,\n async (id, { signal }) => {\n await new Promise((resolve) => setTimeout(resolve, 30))\n if (signal.aborted) throw new Error('request aborted')\n return { id, name: 'User ' + id }\n },\n)\n\nripple.effect(() => console.log(user.value))\n\nuserId.value = 'u2'\nsetTimeout(() => user.reload(), 50)\nsetTimeout(() => {\n user.dispose()\n ripple.dispose()\n}, 100)",
13
+ "name": "Async Resource"
14
+ },
15
+ {
16
+ "id": "basic-signal",
17
+ "code": "import { createRipple } from '@vielzeug/ripple'\n\n// One graph owns state, derived values, effects, and disposal.\nconst ripple = createRipple()\nconst count = ripple.signal(0)\nconst doubled = ripple.computed(() => count.value * 2)\n\nconst stop = ripple.effect(() => {\n console.log({ count: count.value, doubled: doubled.value })\n})\n\ncount.value = 1\ncount.value = 2\n\nstop.dispose()\nripple.dispose()",
18
+ "name": "Create Graph, Signal, Computed & Effect"
19
+ },
20
+ {
21
+ "id": "batch-untrack",
22
+ "code": "import { createRipple } from '@vielzeug/ripple'\n\n// Batch coalesces updates; untrack reads current state without subscribing.\nconst ripple = createRipple()\nconst first = ripple.signal('Ada')\nconst last = ripple.signal('Lovelace')\nconst locale = ripple.signal('en-US')\n\nconst stop = ripple.effect(() => {\n const name = first.value + ' ' + last.value\n const currentLocale = ripple.untrack(() => locale.value)\n console.log({ name, currentLocale })\n})\n\nripple.batch(() => {\n first.value = 'Grace'\n last.value = 'Hopper'\n})\nlocale.value = 'de-DE'\n\nstop.dispose()\nripple.dispose()",
23
+ "name": "Batch & Untrack"
24
+ },
25
+ {
26
+ "id": "effect-options",
27
+ "code": "import { createRipple } from '@vielzeug/ripple'\n\n// Microtask effects coalesce writes until current task ends.\nconst ripple = createRipple()\nconst count = ripple.signal(0)\nconst stop = ripple.effect(\n () => console.log('count:', count.value),\n { name: 'count logger', scheduler: 'microtask' },\n)\n\ncount.value = 1\ncount.value = 2\ncount.value = 3\nconsole.log('writes complete')\n\nqueueMicrotask(() => {\n stop.dispose()\n ripple.dispose()\n})",
28
+ "name": "Microtask Effect"
29
+ },
30
+ {
31
+ "id": "scope-ownership",
32
+ "code": "import { createRipple } from '@vielzeug/ripple'\n\n// Nested work automatically belongs to parent effect run.\nconst ripple = createRipple()\nconst enabled = ripple.signal(true)\nconst count = ripple.signal(0)\n\nconst stop = ripple.effect(() => {\n if (!enabled.value) return\n\n ripple.effect(() => console.log('nested count:', count.value))\n})\n\ncount.value = 1\nenabled.value = false\ncount.value = 2\n\nstop.dispose()\nripple.dispose()",
33
+ "name": "Nested Effect Ownership"
34
+ },
35
+ {
36
+ "id": "store-basics",
37
+ "code": "import { createRipple } from '@vielzeug/ripple'\n\n// signal.update keeps immutable object updates explicit.\nconst ripple = createRipple()\nconst user = ripple.signal({ name: 'Ada', visits: 0 })\nconst greeting = ripple.computed(() => user.value.name + ': ' + user.value.visits)\n\nconst stop = ripple.effect(() => console.log(greeting.value))\n\nuser.update((state) => ({ ...state, visits: state.visits + 1 }))\nuser.value = { name: 'Grace', visits: 5 }\n\nstop.dispose()\nripple.dispose()",
38
+ "name": "Immutable State"
39
+ },
40
+ {
41
+ "id": "watch-selected-value",
42
+ "code": "import { createRipple } from '@vielzeug/ripple'\n\n// Watch receives selected value transitions, not every graph update.\nconst ripple = createRipple()\nconst first = ripple.signal('Ada')\nconst last = ripple.signal('Lovelace')\nconst fullName = ripple.computed(() => first.value + ' ' + last.value)\n\nconst stop = ripple.watch(fullName, (value, previous) => {\n console.log({ previous, value })\n}, { immediate: true })\n\nfirst.value = 'Grace'\nlast.value = 'Hopper'\n\nstop.dispose()\nripple.dispose()",
43
+ "name": "Watch Selected Value"
44
+ }
45
+ ],
46
+ "typeSignatures": {
47
+ "AsyncState": "export type { AsyncState, Resource, ResourceOptions } from './_async';",
48
+ "Resource": "export type { AsyncState, Resource, ResourceOptions } from './_async';",
49
+ "ResourceOptions": "export type { AsyncState, Resource, ResourceOptions } from './_async';",
50
+ "createRipple": "export { createRipple, type Ripple } from './_default';",
51
+ "Ripple": "export { createRipple, type Ripple } from './_default';",
52
+ "BridgedReadable": "export type { BridgedReadable, FromSubscribableOptions } from './_subscribable';",
53
+ "FromSubscribableOptions": "export type { BridgedReadable, FromSubscribableOptions } from './_subscribable';",
54
+ "WatchOptions": "export type { WatchOptions } from './_watch';",
55
+ "RippleComputedCycleError": "export {\n RippleComputedCycleError,\n RippleDisposedResourceError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
56
+ "RippleDisposedResourceError": "export {\n RippleComputedCycleError,\n RippleDisposedResourceError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
57
+ "RippleDisposedRuntimeError": "export {\n RippleComputedCycleError,\n RippleDisposedResourceError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
58
+ "RippleDisposedScopeError": "export {\n RippleComputedCycleError,\n RippleDisposedResourceError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
59
+ "RippleError": "export {\n RippleComputedCycleError,\n RippleDisposedResourceError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
60
+ "RippleInfiniteLoopError": "export {\n RippleComputedCycleError,\n RippleDisposedResourceError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
61
+ "isReactive": "export { isReactive } from './runtime';",
62
+ "Cleanup": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
63
+ "ComputedOptions": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
64
+ "Disposable": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
65
+ "EffectHandle": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
66
+ "EffectOptions": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
67
+ "Equality": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
68
+ "Readable": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
69
+ "RippleErrorContext": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
70
+ "RippleErrorPolicy": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
71
+ "RippleEvent": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
72
+ "RippleOptions": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
73
+ "Scope": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
74
+ "Signal": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
75
+ "SignalOptions": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
76
+ "Subscribable": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
77
+ "Unsubscribe": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';",
78
+ "signal": "export const signal = defaultRipple.signal;",
79
+ "computed": "export const computed = defaultRipple.computed;",
80
+ "effect": "export const effect = defaultRipple.effect;",
81
+ "batch": "export const batch = defaultRipple.batch;",
82
+ "createScope": "export const createScope = defaultRipple.createScope;",
83
+ "untrack": "export const untrack = defaultRipple.untrack;",
84
+ "watch": "export const watch = defaultRipple.watch;",
85
+ "resource": "export const resource = defaultRipple.resource;",
86
+ "fromSubscribable": "export const fromSubscribable = defaultRipple.fromSubscribable;"
87
+ }
88
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "apiSource": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';\nexport { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';\nexport { RuneConfigError, RuneError } from './errors';\nexport type { LazyBinding } from './lazy';\nexport { lazy } from './lazy';\nexport { createLogger } from './logger';\nexport { batchTransport, jsonTransport, redactTransport, remoteTransport, sampleTransport } from './transports';\nexport type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';\nexport { isLevelEnabled, PRIORITY } from './types';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Rune — Structured logging for TypeScript\ndescription: Browser/Node logger with levels, namespaces, pluggable transports, lazy bindings, and timing helpers.\npackage: rune\ncategory: logging\nkeywords: [logging, console, structured, scoped, transports, remote-logging, levels, namespaces, lazy-bindings]\nrelated: [courier, herald, familiar]\nexports:\n [\n createLogger,\n consoleTransport,\n jsonTransport,\n remoteTransport,\n batchTransport,\n sampleTransport,\n redactTransport,\n lazy,\n isLevelEnabled,\n resolveTheme,\n DEFAULT_THEME,\n PRIORITY,\n RuneError,\n RuneConfigError,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"rune\" />\n\n## Why Rune?\n\nPlain `console.log` lacks structure: no log levels, no namespacing, no remote delivery, no way to silence logs in production.\n\n```ts\n// Before — manual approach\nconst path = '/users';\nconsole.log(`[api] GET ${path}`);\n\n// After — Rune\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst api = createLogger({\n namespace: 'api',\n transports: [\n consoleTransport({ level: 'debug' }),\n jsonTransport({ level: 'error' }),\n ],\n});\n\napi.info('request', { method: 'GET', path });\n```\n\n| Feature | Rune | Winston | Pino | console |\n| -------------------- | ------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"rune\" type=\"size\" /> | ~44 kB | ~4 kB | 0 kB |\n| Browser support | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Scoped loggers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Child | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Pluggable transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Built-in factories | <ore-icon name=\"check\" size=\"16\"></ore-icon> Transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Streams | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Structured log entry | <ore-icon name=\"check\" size=\"16\"></ore-icon> `LogEntry` type | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Lazy bindings | <ore-icon name=\"check\" size=\"16\"></ore-icon> `lazy(fn)` | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Styled output | <ore-icon name=\"check\" size=\"16\"></ore-icon> CSS badges | Text only | Text only | Manual |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> (15+) | <ore-icon name=\"x\" size=\"16\"></ore-icon> (5+) | N/A |\n\n<div class=\"decision-callout\">\n\n**Use Rune when** you need isomorphic logging (browser + Node.js), namespaced module loggers, or remote error delivery without a heavy dependency chain.\n\n**Consider alternatives when** you need high-throughput file-based logging (Pino), file rotation (Winston), or your team already uses a logging framework.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/rune\n```\n\n```sh [npm]\nnpm install @vielzeug/rune\n```\n\n```sh [yarn]\nyarn add @vielzeug/rune\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport, lazy } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'server',\n transports: [\n consoleTransport({ timestamp: true }),\n jsonTransport({ level: 'error' }),\n ],\n});\n\nconst requestLog = log.withBindings({\n diagnostics: lazy(() => ({ queueDepth: 0 })),\n requestId: 'abc-123',\n});\n\nrequestLog.info('request', { method: 'GET', path: '/users' });\nconst users = await requestLog.time('load users', () => Promise.resolve(['user-1']));\nconsole.log(users);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Level filtering (`debug` to `off`) with `enabled()` checks, including `fatal` above `error`\n- Immutable config after construction — use `child()` or `withBindings()` to scope\n- Message-first, context-first, and Error-first log calls for application code and adapters\n- `Error` values are auto-serialized to `{ message, name, stack }` — survives JSON.stringify\n- Pinned context bindings via `withBindings({ requestId })` — fields on every line\n- Lazy bindings via `lazy(fn)` — expensive computations gated behind the level check\n- Namespaced child loggers via `createLogger('name')` or `logger.child({ namespace })`\n- Pluggable transport pipeline: console, JSON, remote, batching, sampling, and fail-closed redaction\n- Immutable middleware transforms and filters applied before every transport\n- Structured `time()` wrapper: emits the label as message with `{ duration_ms }` in context\n- `group()` and `groupCollapsed()` wrappers that auto-close on throw/reject\n- `LogEntry.data` — single merged flat object for transports; no manual merging needed\n- Zero dependencies — <PackageInfo package=\"rune\" type=\"size\" /> gzipped\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/) — HTTP client with built-in request/response interception; pipe Rune as a transport to log every API call with structured context\n- [Herald](/herald/) — typed event bus; emit log-level change or flush events across modules without coupling loggers directly\n- [Familiar](/familiar/) — Web Worker pool; use Rune inside task functions to surface structured worker-side logs back to the main thread\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Rune — API Reference\ndescription: API reference for @vielzeug/rune exports, logger methods, configuration types, and transport factories.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------- | ------------------------------------------------ | -------------- | ------------------------------------------------------------ |\n| `createLogger()` | Create an isolated `Logger` instance | Sync | Omitting `transports` defaults to `consoleTransport()` |\n| `lazy(fn)` | Defer a binding value past the level check | Sync | Factory runs on every emit, not once |\n| `isLevelEnabled()` | Utility: test whether a level passes a threshold | Sync | `'off'` always returns `false` |\n| `PRIORITY` | Numeric priority table backing `isLevelEnabled()`| — | Lower number = more verbose |\n| `resolveTheme()` | Merge a partial theme onto the default | Sync | Returns a fully-populated `ResolvedTheme` |\n| `consoleTransport()` | Styled console output | Sync | Theme is resolved once at factory call, not per entry |\n| `jsonTransport()` | NDJSON to stdout or a custom sink | Sync | `process.stdout` is unavailable in browsers |\n| `remoteTransport()` | Asynchronous remote delivery | Async | Pass `onError` for production failure observability |\n| `batchTransport()` | Serialized buffered delivery | Async | Await `dispose()` during shutdown |\n| `sampleTransport()` | Probabilistic forwarding | Sync | Validate the downstream delivery path independently |\n| `redactTransport()` | Recursive sensitive-field masking | Sync | Deeper subtrees are replaced when `maxDepth` is exceeded |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------- | -------------------------------------------------------- |\n| `@vielzeug/rune` | All exports — logger, transport factories, `lazy`, types |\n\n## createLogger(initial?, options?)\n\nCreates an isolated logger instance.\n\n```ts\ncreateLogger(namespace: string, options?: Omit<RuneOptions, 'namespace'>): Logger\ncreateLogger(options?: RuneOptions): Logger\n```\n\n- `string` shorthand sets namespace: `createLogger('api')` or `createLogger('api', { logLevel: 'warn' })`.\n- Each call produces a fully independent instance — no shared mutable state.\n- Default transport is `consoleTransport()` when `transports` is omitted.\n\n> **Note — disposed loggers:** after `dispose()` is called, all log methods (`debug`, `info`, `warn`, `error`, `fatal`), `time()`, and `group()` / `groupCollapsed()` silently no-op. The `fn` callback in `group()` still runs — only the group header is suppressed.\n\n> **Note — fault isolation:** if middleware throws, that entry is dropped; if a transport throws synchronously, sibling transports still run. Asynchronous delivery failures belong to the transport and should be observed through options such as `remoteTransport({ onError })`.\n\n**Returns:** `Logger`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({ logLevel: 'warn', namespace: 'app' });\n\nconst serverLog = createLogger({\n namespace: 'server',\n transports: [\n consoleTransport(),\n jsonTransport({ level: 'error' }),\n ],\n});\n```\n\n## lazy(fn)\n\nDefers evaluation of an expensive binding value until after the level check passes.\nThe factory function is never called when the log level suppresses the entry.\n\n```ts\nlazy(fn: () => unknown): LazyBinding\n```\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst reqLog = log.withBindings({\n diagnostics: lazy(() => buildExpensiveDiagnostics()),\n});\n\nreqLog.debug('trace'); // diagnostics() only called when debug is enabled\n```\n\n**Returns:** `LazyBinding`\n\n## Logger Methods\n\n### Logging\n\nAll five methods use `LogMethod`:\n\n```ts\n(message: string, context?: Bindings): void\n(context: Bindings, message?: string): void\n(error: Error, message?: string): void\n(error: Error, context: Bindings, message?: string): void\n```\n\nMessage-first is preferred for ordinary logs. Context-first supports structured events and adapters. Error-first stores the serialized error under `err`.\n\n```ts\nlog.info('request started', { requestId: 'abc' });\nlog.debug({ type: 'dispatch' }, 'bus:dispatch');\nlog.error(new Error('timeout'), { requestId: 'abc' }, 'request failed');\n```\n\n### Composition\n\n| Method | Returns | What it does |\n| ---------------------- | -------- | ---------------------------------------------------------------- |\n| `child(overrides?)` | `Logger` | Clones config, applies overrides, inherits bindings |\n| `use(middleware)` | `Logger` | Adds an immutable pre-dispatch transform or filter |\n| `withBindings(fields)` | `Logger` | Pins fields to every subsequent call, returns a new child logger |\n\n`child()` transport inheritance:\n\n- Omit `transports` → inherit parent transports (default).\n- Pass `transports: []` → disable all transports on the child.\n- Pass `transports: [...]` → replace entirely with the given list.\n\n`child()` namespace joining:\n\n- `parent.child({ namespace: 'auth' })` on a logger with namespace `'api'` produces `'api.auth'`.\n- Omit `namespace` → inherits parent namespace unchanged.\n\n### Utilities\n\n| Method | Returns | Description |\n| ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `enabled(level)` | `boolean` | True if entries at this level pass the configured threshold |\n| `time(label, fn, level?)` | `T` | Measures sync/async execution; emits at `level` (default `'debug'`), label as message, `{ duration_ms }` in `data`. When `fn` throws or rejects, `{ err }` is also included. |\n| `group(label, fn, level?)` | `T` | Wraps callback in `console.group`; closes even on throw/reject. Pass `level` to gate the group header on the configured threshold (e.g. `'debug'` suppresses when `logLevel` is `'warn'`). |\n| `groupCollapsed(label, fn, level?)` | `T` | Same as `group`, using `console.groupCollapsed`. |\n| `dispose()` | `void` | Silences all subsequent log calls on this logger instance. Idempotent. |\n\n### Properties\n\n| Property | Type | Description |\n| ------------------ | -------------------------- | ------------------------------------------------------------------ |\n| `logLevel` | `LogLevel` | Active log level threshold |\n| `namespace` | `string` | Effective namespace string |\n| `transports` | `readonly Transport[]` | Transport pipeline snapshot |\n| `middleware` | `readonly LogMiddleware[]` | Middleware pipeline snapshot |\n| `bindings` | `Readonly<Bindings>` | Snapshot of currently pinned fields |\n| `disposalSignal` | `AbortSignal` | Aborted when `dispose()` is called. Use to tie external lifetimes. |\n| `disposed` | `boolean` | `true` after `dispose()` has been called |\n| `[Symbol.dispose]` | `() => void` | Delegates to `dispose()`. Enables `using` declarations. |\n\n## Transport Factories\n\n### consoleTransport(options?)\n\n```ts\nconsoleTransport(options?: ConsoleTransportOptions): Transport\n```\n\nWrites styled output to the browser console (CSS badges) or Node terminal (plain text). This is the default transport.\n\n| Option | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\nimport { inspect } from 'node:util';\n\nconst log = createLogger({\n transports: [consoleTransport({ level: 'info', timestamp: true, inspectFn: inspect })],\n});\n```\n\n### jsonTransport(options?)\n\n```ts\njsonTransport(options?: JsonTransportOptions): Transport\n```\n\nOutputs newline-delimited JSON (NDJSON) to `stdout` or a custom function. Useful for server-side log aggregation pipelines (ELK, Datadog, etc.).\n\nEach line is a flat JSON object with `level`, `time` (ISO), and optional `ns`, `msg`, plus all merged context fields.\n\n| Option | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | -------------------------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names for aggregator compatibility (e.g. `'severity'` for Datadog) |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info('request', { path: '/users', status: 200 });\n// {\"path\":\"/users\",\"status\":200,\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"msg\":\"request\"}\n```\n\n### remoteTransport(options)\n\n```ts\nremoteTransport(options: RemoteTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| --------- | ------------------------------------------------------------- | ------------- | ------------------------------ |\n| `handler` | `(level, data) => void \\| Promise<unknown>` | required | Remote delivery function |\n| `level` | `LogLevel` | `'debug'` | Minimum forwarded level |\n| `env` | `'development' \\| 'production'` | auto-detected | Payload environment |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | dev warning | Delivery failure observer |\n\n**Returns:** A fire-and-forget `Transport`. Pass `onError` whenever production delivery failures require monitoring.\n\n```ts\nimport { remoteTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({\n handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' }),\n onError: (error) => console.error('log delivery failed', error),\n});\n```\n\n### batchTransport(options)\n\n```ts\nbatchTransport(options: BatchTransportOptions): BatchHandle\n```\n\n| Option | Type | Default | Description |\n| -------------- | --------------------------------------------- | --------- | ----------------------------------- |\n| `onFlush` | `(entries) => void \\| Promise<void>` | required | Serialized batch delivery |\n| `onFlushError` | `(entries, error) => void` | — | Delivery failure observer |\n| `interval` | `number` | `5000` | Flush interval in milliseconds |\n| `maxSize` | `number` | `50` | Entries that trigger an early flush |\n| `maxBuffer` | `number` | unbounded | Hard in-memory limit |\n| `level` | `LogLevel` | `'debug'` | Minimum buffered level |\n\n**Returns:** `BatchHandle` with `transport`, `flush()`, `dispose()`, `disposed`, and `[Symbol.asyncDispose]`. Await `dispose()` during shutdown.\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n onFlush: (entries) => fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' }),\n});\nconst log = createLogger({ transports: [batch.transport] });\nawait batch.dispose();\n```\n\n### sampleTransport(options)\n\n```ts\nsampleTransport(options: SampleTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| ----------- | ----------- | --------- | ------------------------------ |\n| `rate` | `number` | required | Forwarding fraction from 0–1 |\n| `transport` | `Transport` | required | Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level before sampling |\n\n**Returns:** A sampling `Transport`.\n\n```ts\nimport { remoteTransport, sampleTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({ handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data) }) });\nconst sampled = sampleTransport({ rate: 0.1, transport: remote });\n```\n\n### redactTransport(options)\n\n```ts\nredactTransport(options: RedactTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| ------------- | ------------------- | -------------- | ------------------------------------------------ |\n| `keys` | `readonly string[]` | required | Exact field names redacted at any inspected depth |\n| `transport` | `Transport` | required | Downstream transport |\n| `maxDepth` | `number` | `20` | Maximum nested object depth inspected |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement for keys and deeper subtrees |\n\n**Returns:** A non-mutating redaction `Transport`. When `maxDepth` is exceeded, the whole deeper subtree is replaced; uninspected values are never forwarded.\n\n```ts\nimport { jsonTransport, redactTransport } from '@vielzeug/rune';\n\nconst safeJson = redactTransport({ keys: ['password', 'token'], transport: jsonTransport() });\n```\n\n## Utilities\n\n### isLevelEnabled(threshold, level)\n\n```ts\nisLevelEnabled(threshold: LogLevel, level: LogLevel): boolean\n```\n\nReturns `true` when `level` is at or above `threshold`. Always returns `false` when `level` is `'off'`. Useful for building custom transports that respect level filtering.\n\n```ts\nimport { isLevelEnabled } from '@vielzeug/rune';\n\nisLevelEnabled('warn', 'error'); // true\nisLevelEnabled('warn', 'info'); // false\nisLevelEnabled('debug', 'off'); // false\n```\n\n### resolveTheme(override?)\n\n```ts\nresolveTheme(override: ConsoleTheme | undefined): ResolvedTheme\n```\n\nDeep-merges a partial `ConsoleTheme` override onto `DEFAULT_THEME`. Returns a fully-populated `ResolvedTheme` where every level and every field is present. Used internally by `consoleTransport()` — call directly when building a custom transport that needs to honour theme overrides.\n\n```ts\nimport { resolveTheme } from '@vielzeug/rune';\n\nconst theme = resolveTheme({ warn: { badge: '⚡' } });\n// theme.warn.badge === '⚡', theme.warn.bg === DEFAULT_THEME.warn.bg (unchanged)\n```\n\n### DEFAULT_THEME\n\nThe built-in badge and namespace colour definitions used by `consoleTransport()`. Override per-transport via `ConsoleTransportOptions.theme`.\n\n### PRIORITY\n\n```ts\nPRIORITY: Record<LogLevel, number>\n```\n\nNumeric priority for each level (`debug: 0`, `info: 1`, `warn: 2`, `error: 3`, `fatal: 4`, `off: 5`) — lower is more verbose. Exported for transport authors building custom level-comparison logic; `isLevelEnabled()` is built directly on top of it.\n\n## Types\n\n### LogType\n\n`'debug' | 'error' | 'fatal' | 'info' | 'warn'`\n\n### LogLevel\n\n`LogType | 'off'` — threshold order: `debug < info < warn < error < fatal < off`\n\n### Bindings\n\n`Record<string, unknown>` — Key-value context pinned via `withBindings()` or passed per-call.\n\n### LogEntry\n\nThe structured record produced by every log call and dispatched to all transports.\n\n| Field | Type | Description |\n| ----------- | -------------------- | ------------------------------------------------------------------------ |\n| `data` | `Readonly<Bindings>` | Merged result of pinned bindings and per-call context — already resolved |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string` | Effective namespace at time of call |\n| `timestamp` | `Date` | Exact moment of the call, shared across transports |\n\n### Transport\n\n```ts\ntype Transport = (entry: LogEntry) => void;\n```\n\nReceives every `LogEntry` that passes the logger's level threshold. Responsible for its own formatting, delivery, and per-transport level filtering.\n\n### ConsoleThemeEntry\n\n```ts\ntype ConsoleThemeEntry = {\n badge: string;\n bg: string;\n border: string;\n color: string;\n};\n```\n\nPer-level style definition for the console transport. All fields are optional when providing a level override — unspecified fields fall back to the default theme.\n\n### ConsoleTheme\n\n```ts\ntype ConsoleTheme = Partial<Record<LogType | 'group' | 'ns', Partial<ConsoleThemeEntry>>>;\n```\n\nPartial theme overrides merged on top of the default theme. Each level entry is also partial — only specify the fields you want to change.\n\n### ResolvedTheme\n\n`Record<LogType | 'group' | 'ns', ConsoleThemeEntry>` — fully resolved theme with all fields populated.\n\n### RuneOptions\n\n| Field | Type | Default | Description |\n| ------------ | -------------- | ---------------------- | ---------------------------- |\n| `logLevel` | `LogLevel?` | `'debug'` | Logger level threshold |\n| `namespace` | `string?` | `''` | Namespace prefix |\n| `transports` | `Transport[]?` | `[consoleTransport()]` | Transport pipeline |\n| `middleware` | `LogMiddleware[]?` | `[]` | Pre-dispatch transform/filter pipeline |\n| `bindings` | `Bindings?` | `{}` | Initial pinned bindings |\n\n### LogMethod\n\n```ts\ntype LogMethod = {\n (message: string, context?: Bindings): void;\n (context: Bindings, message?: string): void;\n (error: Error, message?: string): void;\n (error: Error, context: Bindings, message?: string): void;\n};\n```\n\nMessage-first, context-first, and Error-first calls are supported. Top-level `Error` values are serialized to `{ message, name, stack }`.\n\n### LazyBinding\n\nOpaque type returned by `lazy()`. Pass as a value inside `withBindings()`. The factory is only called when the entry is actually emitted (after the level check passes).\n\n### Logger\n\nThe full interface returned by `createLogger()`:\n\n```ts\ntype Logger = {\n [Symbol.dispose]: () => void;\n readonly bindings: Readonly<Bindings>;\n child: (overrides?: RuneOptions) => Logger;\n debug: LogMethod;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n enabled: (type: LogLevel) => boolean;\n error: LogMethod;\n fatal: LogMethod;\n group: <T>(label: string, fn: () => T, level?: LogType) => T;\n groupCollapsed: <T>(label: string, fn: () => T, level?: LogType) => T;\n info: LogMethod;\n readonly logLevel: LogLevel;\n readonly middleware: readonly LogMiddleware[];\n readonly namespace: string;\n time: <T>(label: string, fn: () => T, level?: LogType) => T;\n readonly transports: readonly Transport[];\n use: (middleware: LogMiddleware) => Logger;\n warn: LogMethod;\n withBindings: (bindings: Bindings) => Logger;\n};\n```\n\n### ConsoleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n### JsonTransportOptions\n\n| Field | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | ------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names (e.g. `level: 'severity'` for Datadog) |\n\n### LogMiddleware\n\n```ts\ntype LogMiddleware = (entry: LogEntry) => LogEntry | null;\n```\n\nTransforms an entry before all transports or returns `null` to drop it.\n\n### RemoteLogData and RemoteTransportOptions\n\n```ts\ntype RemoteLogData = {\n data?: Readonly<Bindings>;\n env: 'development' | 'production';\n level: LogType;\n message?: string;\n namespace?: string;\n timestamp: string;\n};\n\ntype RemoteTransportOptions = {\n env?: 'development' | 'production';\n handler: (type: LogType, data: RemoteLogData) => void | Promise<unknown>;\n level?: LogLevel;\n onError?: (error: unknown, data: RemoteLogData) => void;\n};\n```\n\n### BatchHandle and BatchTransportOptions\n\n```ts\ntype BatchHandle = {\n [Symbol.asyncDispose]: () => Promise<void>;\n dispose: () => Promise<void>;\n readonly disposed: boolean;\n flush: () => Promise<void>;\n transport: Transport;\n};\n\ntype BatchTransportOptions = {\n interval?: number;\n level?: LogLevel;\n maxBuffer?: number;\n maxSize?: number;\n onFlush: (entries: LogEntry[]) => void | Promise<void>;\n onFlushError?: (entries: LogEntry[], error: unknown) => void;\n};\n```\n\n### SampleTransportOptions\n\n```ts\ntype SampleTransportOptions = {\n level?: LogLevel;\n rate: number;\n transport: Transport;\n};\n```\n\n### RedactTransportOptions\n\n```ts\ntype RedactTransportOptions = {\n keys: readonly string[];\n maxDepth?: number;\n replacement?: string;\n transport: Transport;\n};\n```\n\nDepth-limited subtrees are replaced entirely so sensitive values are never forwarded uninspected.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `RuneError` | Base class for all rune-originated errors | `instanceof RuneError` narrows all Rune errors |\n| `RuneConfigError` | Logger or transport configured with invalid options | — |\n",
6
+ "usage": "---\ntitle: Rune — Usage Guide\ndescription: Configuration, transports, scoped loggers, lazy bindings, timers, groups, and best practices for Rune.\n---\n\n[[toc]]\n\n::: tip New to Rune?\nStart with the [Overview](./index.md), then use this page for detailed usage patterns.\n:::\n\n## Basic Usage\n\nUse `createLogger()` to create an isolated logger instance.\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\n\nconst appLog = createLogger();\nconst apiLog = createLogger({ namespace: 'api' });\nconst authLog = createLogger('auth'); // shorthand namespace\n```\n\nEach `createLogger()` call is fully independent with its own transport pipeline.\n\nThe two-arg shorthand combines namespace and options cleanly:\n\n```ts\nconst log = createLogger('api', { logLevel: 'warn', transports: [transport] });\n```\n\n## Transports\n\nTransports are the delivery layer. Every `LogEntry` that passes the logger's level threshold is dispatched to each transport in order. Transports handle their own formatting, level filtering, and delivery.\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n transports: [\n consoleTransport({ timestamp: true }),\n jsonTransport({ level: 'error' }),\n ],\n});\n```\n\nWhen `transports` is omitted, `consoleTransport()` is used automatically.\n\n### Built-in Transport Factories\n\n| Factory | Use case |\n| -------------------- | --------------------------------------------- |\n| `consoleTransport()` | Styled console output (default) |\n| `jsonTransport()` | NDJSON for server-side log aggregation |\n| `remoteTransport()` | Asynchronous browser or server delivery |\n| `batchTransport()` | Serialized buffered delivery with flush |\n| `sampleTransport()` | Probabilistic volume reduction |\n| `redactTransport()` | Recursive sensitive-field and subtree masking |\n\n### Node.js: Structured JSON Logging\n\nFor server-side log pipelines (ELK, Datadog, CloudWatch), `jsonTransport` emits NDJSON to stdout:\n\n```ts\nimport { jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info('request', { path: '/users', status: 200 });\n// Outputs: {\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"path\":\"/users\",\"status\":200,\"msg\":\"request\"}\n```\n\n### Remote Delivery, Batching, and Redaction\n\nCompose transport wrappers around the delivery boundary. Handle remote failures explicitly in production and dispose the batch during shutdown.\n\n```ts\nimport { batchTransport, createLogger, redactTransport, remoteTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({\n handler: (_level, payload) =>\n fetch('/api/logs', { body: JSON.stringify(payload), method: 'POST' }).then((response) => {\n if (!response.ok) throw new Error(`log delivery failed: ${response.status}`);\n }),\n level: 'warn',\n onError: (error) => console.error('log delivery failed', error),\n});\nconst batch = batchTransport({\n onFlush: async (entries) => {\n const response = await fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' });\n if (!response.ok) throw new Error(`batch delivery failed: ${response.status}`);\n },\n});\nconst log = createLogger({\n transports: [redactTransport({ keys: ['password', 'token'], transport: remote })],\n});\nconst batchedLog = createLogger({\n transports: [redactTransport({ keys: ['password', 'token'], transport: batch.transport })],\n});\n\nlog.error('direct delivery');\nbatchedLog.error('buffered delivery');\nawait batch.dispose();\n```\n\n`redactTransport()` fails closed: when `maxDepth` is exceeded, the entire deeper subtree is replaced rather than forwarded uninspected.\n\n## Configuration\n\nUse `child()` to derive immutable logger variants.\n\n```ts\nconst log = createLogger({ namespace: 'app' });\nconst AppLog = log.child({\n logLevel: 'warn',\n namespace: 'App',\n // transports inherited from parent by default\n // pass transports: [] to disable all, or transports: [...] to replace\n});\n\n// Individual getters — no config snapshot\nconsole.log(AppLog.logLevel); // 'warn'\nconsole.log(AppLog.namespace); // 'App'\nconsole.log(AppLog.transports); // [...]\n```\n\nLevel threshold order: `debug` < `info` < `warn` < `error` < `fatal` < `off`\n\n## Middleware\n\nUse middleware for transformations or filters that must apply once before every transport. Middleware runs in order; returning `null` drops the entry.\n\n```ts\nconst log = createLogger({\n middleware: [(entry) => ({ ...entry, data: { ...entry.data, environment: 'production' } })],\n transports: [consoleTransport(), jsonTransport()],\n});\n\nconst errorsOnly = log.use((entry) => (entry.level === 'error' || entry.level === 'fatal' ? entry : null));\n```\n\n`use()` returns a new logger and leaves its parent unchanged. A throwing middleware is isolated and drops only the affected entry.\n\n## Call Signature\n\nAll log methods support message-first, context-first, and error-first calls:\n\n```ts\nlog.info('message');\nlog.info('message', { key: 'value' });\nlog.debug({ type: 'dispatch', id: 42 }, 'bus:dispatch');\nlog.error(new Error('boom'), { requestId: 'abc' }, 'request failed');\n```\n\nUse message-first for ordinary application logs. Context-first avoids synthetic messages for structured events and matches observation callbacks such as `tap((event) => log.debug(event))`. Error-first automatically stores the serialized error under `err`.\n\nPer-call context is shallow-merged with `withBindings()` bindings into `entry.data`. `Error` values are serialized to `{ message, name, stack }`.\n\n## Logging Methods\n\n```ts\nconst log = createLogger();\nlog.debug('debug details');\nlog.info('server started', { port: 3000 });\nlog.warn('cache stale');\nlog.error('request failed', { err: new Error('timeout') }); // Error auto-serialized in context\nlog.fatal('terminating', { service: 'db' }); // above error, use for unrecoverable state\n```\n\nUse `enabled()` to avoid expensive payload construction before the level check:\n\n```ts\nif (log.enabled('debug')) {\n log.debug('diagnostics', { diagnostics: buildLargePayload() });\n}\n```\n\nOr use `lazy()` to let Rune gate it automatically:\n\n```ts\nconst reqLog = log.withBindings({ diagnostics: lazy(() => buildLargePayload()) });\nreqLog.debug('diagnostics'); // buildLargePayload() only called when debug is enabled\n```\n\n## Pinned Bindings\n\n`withBindings(fields)` returns a child logger where the given fields are merged into every log call. This is the idiomatic way to attach per-request or per-user context.\n\n```ts\nconst api = createLogger({ namespace: 'api' });\n\nconst reqLog = api.withBindings({ requestId: 'abc-123', userId: 42 });\nreqLog.info('GET /users'); // always includes requestId and userId\nreqLog.warn('query took 2s', { slow: true }); // call-site fields merged in\n```\n\nThe parent logger is not affected. Bindings stack additively through chained `withBindings()` calls:\n\n```ts\nconst base = log.withBindings({ service: 'api' });\nconst req = base.withBindings({ requestId: 'xyz' });\n// req emits both service and requestId on every call\n```\n\nThe `bindings` getter returns a defensive snapshot:\n\n```ts\nconsole.log(reqLog.bindings); // { requestId: 'abc-123', userId: 42 }\n```\n\n## Lazy Bindings\n\n`lazy(fn)` defers evaluation of a binding value until after the level check passes. The factory is never called when the entry would be suppressed.\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst log = createLogger().withBindings({\n // Only called when debug entries are emitted\n snapshot: lazy(() => JSON.stringify(getFullAppState())),\n // Regular values are always included as-is\n service: 'api',\n});\n\nlog.debug('state trace'); // snapshot() only called here\nlog.warn('cache miss'); // snapshot() NOT called — warn doesn't need it\n```\n\nLazy bindings are resolved on every emitted call, not cached:\n\n```ts\nconst counter = { n: 0 };\nconst log = createLogger().withBindings({ tick: lazy(() => ++counter.n) });\n\nlog.info('a'); // tick: 1\nlog.info('b'); // tick: 2\n```\n\n## Child Loggers\n\n`child(overrides?)` creates a new logger scoped to a namespace, level, or transport set. Use it to create module-level or service-level loggers.\n\n```ts\nconst api = createLogger({ namespace: 'api' });\nconst auth = api.child({ namespace: 'auth' }); // → 'api.auth' (dot-joined automatically)\n\napi.info('GET /users');\nauth.warn('token expiring');\n```\n\n`child(overrides?)` clones current config and applies overrides. Transports are inherited by default.\n\n```ts\nconst base = createLogger({ logLevel: 'info', namespace: 'app' });\nconst verbose = base.child({ logLevel: 'debug' }); // inherits transports\n\n// Replace transports entirely on the child\nconst silent = base.child({ transports: [] }); // no output\n\n// Override with a different transport set\nconst jsonChild = base.child({ transports: [jsonTransport()] });\n```\n\nChild and parent configs remain independent after creation.\n\n## Timing\n\n`time(label, fn, level?)` measures execution time of sync or async functions. Emits a structured entry with `{ duration_ms }` in `data` and `label` as the message. When `fn` throws or rejects, the entry also includes `{ err }` with the serialized error.\n\n```ts\n// Sync\nconst result = log.time('parse', () => parseDocument(input));\n// Emits: { level: 'debug', message: 'parse', data: { duration_ms: 2.4 } }\n\n// Async\nconst users = await log.time('db.users', () => db.query('SELECT * FROM users'));\n// Emits even on rejection, with { err } included in data\n\n// Custom level\nlog.time('health-check', () => ping(), 'info');\n\n// Skipped when logLevel is 'off', but fn still executes\n```\n\n## Groups\n\n`group(label, fn, level?)` and `groupCollapsed(label, fn, level?)` wrap a callback in a console group, ensuring `groupEnd` is called even when the callback throws or rejects.\n\n```ts\nawait log.groupCollapsed('Job', async () => {\n await log.time('process', () => runJob());\n log.info('Done');\n});\n\n// Gate the group header on a log level — suppresses when logLevel is above 'debug'\nlog.group(\n 'verbose trace',\n () => {\n log.debug('internal state', { state });\n },\n 'debug',\n);\n```\n\nWhen `logLevel` is `'off'`, the group wrapper is bypassed but the callback still executes. When a `level` is provided and it is below the configured threshold, the group header is skipped but the callback still runs.\n\n## Testing\n\nUse a test transport to assert log entries without mocking `console`. This approach is more robust and does not require spy cleanup:\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createLogger } from '@vielzeug/rune';\nimport type { LogEntry, Transport } from '@vielzeug/rune';\n\nfunction createTestTransport() {\n const entries: LogEntry[] = [];\n const transport: Transport = (entry) => entries.push(entry);\n return { entries, transport };\n}\n\nit('logs errors when enabled', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'error', transports: [transport] });\n\n log.error('boom');\n\n expect(entries).toHaveLength(1);\n expect(entries[0].level).toBe('error');\n expect(entries[0].message).toBe('boom');\n});\n\nit('suppresses debug when logLevel is warn', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'warn', transports: [transport] });\n\n log.debug('silent');\n log.warn('loud');\n\n expect(entries).toHaveLength(1);\n});\n```\n\nYou can still spy on `console` methods when testing `consoleTransport` output directly:\n\n```ts\nimport { afterEach, expect, it, vi } from 'vitest';\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\n\nafterEach(() => vi.restoreAllMocks());\n\nit('writes error to console.error', () => {\n const spy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const log = createLogger({ logLevel: 'error', transports: [consoleTransport({ timestamp: false })] });\n\n log.error('boom');\n\n expect(spy).toHaveBeenCalled();\n});\n```\n\n## Framework Integration\n\nRune is framework-agnostic and works as a module-level singleton or a context-injected instance.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useState, useContext } from 'react';\nimport { createLogger } from '@vielzeug/rune';\n\nconst LogContext = createContext(createLogger({ namespace: 'app' }));\n\nfunction useLogger() {\n return useContext(LogContext);\n}\n\nfunction App() {\n const [requestLogger] = useState(() => createLogger({ namespace: 'app' }).withBindings({ userId: '42' }));\n return (\n <LogContext.Provider value={requestLogger}>\n <Dashboard />\n </LogContext.Provider>\n );\n}\n\nfunction Dashboard() {\n const log = useLogger();\n log.info('Dashboard mounted');\n return <div>Dashboard</div>;\n}\n```\n\n```ts [Vue 3]\nimport { inject, provide } from 'vue';\nimport { createLogger, type Logger } from '@vielzeug/rune';\n\nconst LoggerKey = Symbol('logger');\n\nfunction provideLogger(namespace: string) {\n const logger = createLogger({ namespace });\n provide(LoggerKey, logger);\n return logger;\n}\n\nfunction useLogger(): Logger {\n const logger = inject<Logger>(LoggerKey);\n if (!logger) throw new Error('Logger not provided');\n return logger;\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { setContext, getContext } from 'svelte';\n import { createLogger } from '@vielzeug/rune';\n\n const logger = createLogger({ namespace: 'app' });\n setContext('logger', logger);\n</script>\n\n<!-- Child component -->\n<script lang=\"ts\">\n import { getContext } from 'svelte';\n import type { Logger } from '@vielzeug/rune';\n\n const logger = getContext<Logger>('logger');\n logger.info('component mounted');\n</script>\n```\n\n:::\n\n### Pitfalls\n\n- **React:** Creating the logger without a stable initializer recreates it on every re-render. Use `useState(() => createLogger(...))`.\n- **Vue 3:** `inject()` must be called at the top level of `setup()`, not inside callbacks.\n- **Svelte:** `getContext()` must be called synchronously during component initialization.\n\n## Working with Other Vielzeug Libraries\n\n### With Courier\n\n```ts\nimport { createCourier, withLogging } from '@vielzeug/courier';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'courier' });\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withLogging({ logger: (message, meta) => log.debug(message, meta) })],\n});\n```\n\n### With Herald\n\n```ts\nimport { createBus } from '@vielzeug/herald';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'bus' });\nconst bus = createBus<AppEvents>({\n onDispatch: (event, payload) => log.debug('dispatched', { event, payload }),\n onError: (err, event) => log.error(`handler error in \"${event}\"`, { err }),\n});\n```\n\n## Best Practices\n\n- Create one child logger per module boundary using `createLogger('module.name')` or `log.child({ namespace: 'module.name' })`.\n- Use `withBindings()` to pin request/session context instead of repeating fields on each call.\n- Use `lazy()` for expensive diagnostics bindings only needed at `debug` level.\n- Set `logLevel` from environment (`'debug'` in dev, `'warn'` or `'error'` in prod).\n- Use `enabled()` before expensive payload construction that `lazy()` cannot defer.\n- Configure transports at the application root; pass scoped loggers via DI or context.\n- To style console output, pass `consoleTransport({ theme })` explicitly in `transports`.\n- Use `fatal()` only for genuinely unrecoverable states.\n",
7
+ "examples": "---\ntitle: Rune — Examples\ndescription: Practical examples and recipes for rune.\n---\n\n## Examples\n\n- [Module Logger Pattern](./examples/module-logger-pattern.md)\n- [Child Logger Overrides](./examples/child-logger-overrides.md)\n- [Production Setup](./examples/production-setup.md)\n- [Timing And Grouping](./examples/timing-and-grouping.md)\n- [React Integration](./examples/react-integration.md)\n- [Request Middleware](./examples/request-middleware.md)\n- [Testing](./examples/testing.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "basic-logging",
12
+ "code": "import { createLogger } from '@vielzeug/rune'\n\nconst log = createLogger()\n\n// message-only\nlog.debug('app starting')\nlog.info('ready')\n\n// message + structured context\nlog.info('server listening', { port: 3000 })\nlog.warn('retrying request', { retries: 3 })\n\n// Pass Error as a context field — auto-serialized to { message, name, stack }\nconst err = new Error('connection refused')\nlog.error('service unavailable', { err })\nlog.error('request failed', { err, requestId: 'r-001' })\n\nconsole.log('(Open DevTools console to see styled output)')",
13
+ "name": "Basic Logging"
14
+ },
15
+ {
16
+ "id": "lazy-and-timing",
17
+ "code": "import { createLogger, lazy } from '@vielzeug/rune'\n\nconst entries = []\nconst log = createLogger({ transports: [(e) => entries.push(e)] })\n\n// lazy() defers factory evaluation until after the level check\nlet callCount = 0\nconst reqLog = log.withBindings({\n snapshot: lazy(() => ({ n: ++callCount, size: 1024 })),\n})\n\nreqLog.debug('trace') // snapshot() called — callCount becomes 1\nreqLog.info('step 2') // snapshot() called — callCount becomes 2\n\n// Suppress debug: lazy factory is never called\nconst quietLog = log.child({ logLevel: 'warn' })\nconst quietReq = quietLog.withBindings({ val: lazy(() => ++callCount) })\nquietReq.debug('not emitted') // factory skipped\n\nconsole.log('Factory calls:', callCount) // 2, not 3\n\n// time() measures execution and emits { duration_ms } in data\nconst parsed = log.time('parse', () => JSON.parse('[1,2,3]'))\nconsole.log('Parsed:', parsed)\nconsole.log('Timer data:', entries[entries.length - 1].data)\n\n// When the timed fn throws, { err } is also included in data\ntry {\n log.time('risky', () => { throw new Error('oops') })\n} catch {}\nconsole.log('Error data:', entries[entries.length - 1].data)\n\n// dispose() marks the logger as disposed; subsequent calls become no-ops\nlog.dispose()\nlog.info('silenced') // no-op\nconsole.log('disposed:', log.disposed) // true",
18
+ "name": "Lazy Bindings & Timing"
19
+ },
20
+ {
21
+ "id": "level-filtering",
22
+ "code": "import { createLogger } from '@vielzeug/rune'\n\nconst entries = []\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'app',\n transports: [(e) => entries.push(e)],\n})\n\n// Threshold order: debug < info < warn < error < fatal < off\nlog.debug('msg')\nlog.info('msg')\nlog.warn('msg')\nlog.error('msg')\nlog.fatal('msg')\nconsole.log('All levels:', entries.map((e) => e.level))\nentries.length = 0\n\n// child() with raised threshold — debug and info are suppressed\nconst prodLog = log.child({ logLevel: 'warn' })\nprodLog.debug('suppressed')\nprodLog.info('suppressed')\nprodLog.warn('passes')\nprodLog.error('passes')\nconsole.log('Threshold warn:', entries.map((e) => e.level))\nentries.length = 0\n\n// Individual getters expose config without a snapshot object\nconsole.log('log.logLevel:', log.logLevel) // 'debug'\nconsole.log('prodLog.logLevel:', prodLog.logLevel) // 'warn'\nconsole.log('log.namespace:', log.namespace) // 'app'\n\n// enabled() guards expensive payload construction\nconsole.log('debug enabled:', log.enabled('debug')) // true\nconsole.log('debug enabled (prod):', prodLog.enabled('debug')) // false",
23
+ "name": "Level Filtering"
24
+ },
25
+ {
26
+ "id": "lifecycle",
27
+ "code": "import { createLogger } from '@vielzeug/rune';\n\n// Two-arg shorthand: namespace + options\nconst log = createLogger('api', { logLevel: 'debug' });\n\nlog.info('logger created');\nlog.debug('request start', { url: '/health' });\n\n// disposed logger silences all subsequent calls\nlog.dispose();\nlog.info('this is silenced — no output');\n\nconsole.log('log.disposed:', log.disposed);\n\n// using-declaration auto-disposes at end of scope\n{\n using scoped = createLogger('scoped', { logLevel: 'debug' });\n scoped.info('scoped logger active');\n} // scoped.dispose() called automatically here\n\n// disposalSignal aborts when dispose() is called\nconst monitored = createLogger('monitored');\nmonitored.disposalSignal.addEventListener('abort', () => {\n console.log('monitored logger was disposed');\n});\nmonitored.dispose();\n",
28
+ "name": "Logger Lifecycle & Disposal"
29
+ },
30
+ {
31
+ "id": "scoped-loggers",
32
+ "code": "import { createLogger } from '@vielzeug/rune'\n\nconst log = createLogger()\n\n// child() dot-joins namespaces automatically\nconst api = log.child({ namespace: 'api' }) // 'api'\nconst auth = api.child({ namespace: 'auth' }) // 'api.auth'\nconst worker = api.child({ namespace: 'worker' }) // 'api.worker'\n\n// Individual getters — no config snapshot\nconsole.log('root:', log.namespace) // ''\nconsole.log('api:', api.namespace) // 'api'\nconsole.log('auth:', auth.namespace) // 'api.auth'\nconsole.log('worker:', worker.namespace) // 'api.worker'\n\n// withBindings() pins fields to every call\nconst reqLog = auth.withBindings({ requestId: 'r-001', userId: 'u-42' })\nreqLog.info('token check')\nreqLog.warn('token refreshed', { expired: false })\n\nconsole.log('bindings snapshot:', reqLog.bindings)\n\n// enabled() checks whether a level passes the logger threshold\nconsole.log('debug enabled:', api.enabled('debug')) // true (default level is debug)\nconst warnLog = api.child({ logLevel: 'warn' })\nconsole.log('warnLog debug:', warnLog.enabled('debug')) // false\n\nconsole.log('(Open DevTools console to see styled output)')",
33
+ "name": "Scoped Loggers"
34
+ },
35
+ {
36
+ "id": "transport-pipeline",
37
+ "code": "import { createLogger } from '@vielzeug/rune'\n\n// A custom inline transport captures entries synchronously\nconst entries = []\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'app',\n transports: [(entry) => entries.push(entry)],\n})\n\nlog.info('request', { path: '/users', method: 'GET' })\nlog.warn('cache miss')\nlog.error('request failed', { err: new Error('timeout') })\n\n// Inspect the structured LogEntry objects captured by the transport\nentries.forEach((e, i) => {\n console.log('Entry ' + (i + 1) + ' [' + e.level + ']:', JSON.stringify({\n namespace: e.namespace,\n message: e.message,\n data: e.data,\n }))\n})",
38
+ "name": "Transport Pipeline"
39
+ }
40
+ ],
41
+ "typeSignatures": {
42
+ "ConsoleTheme": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';",
43
+ "ConsoleThemeEntry": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';",
44
+ "ConsoleTransportOptions": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';",
45
+ "ResolvedTheme": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';",
46
+ "consoleTransport": "export { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';",
47
+ "DEFAULT_THEME": "export { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';",
48
+ "resolveTheme": "export { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';",
49
+ "RuneConfigError": "export { RuneConfigError, RuneError } from './errors';",
50
+ "RuneError": "export { RuneConfigError, RuneError } from './errors';",
51
+ "LazyBinding": "export type { LazyBinding } from './lazy';",
52
+ "lazy": "export { lazy } from './lazy';",
53
+ "createLogger": "export { createLogger } from './logger';",
54
+ "batchTransport": "export { batchTransport, jsonTransport, redactTransport, remoteTransport, sampleTransport } from './transports';",
55
+ "jsonTransport": "export { batchTransport, jsonTransport, redactTransport, remoteTransport, sampleTransport } from './transports';",
56
+ "redactTransport": "export { batchTransport, jsonTransport, redactTransport, remoteTransport, sampleTransport } from './transports';",
57
+ "remoteTransport": "export { batchTransport, jsonTransport, redactTransport, remoteTransport, sampleTransport } from './transports';",
58
+ "sampleTransport": "export { batchTransport, jsonTransport, redactTransport, remoteTransport, sampleTransport } from './transports';",
59
+ "BatchHandle": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
60
+ "BatchTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
61
+ "Bindings": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
62
+ "JsonTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
63
+ "LogEntry": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
64
+ "Logger": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
65
+ "LogLevel": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
66
+ "LogMethod": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
67
+ "LogMiddleware": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
68
+ "LogType": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
69
+ "RedactTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
70
+ "RemoteLogData": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
71
+ "RemoteTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
72
+ "RuneOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
73
+ "SampleTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
74
+ "Transport": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
75
+ "isLevelEnabled": "export { isLevelEnabled, PRIORITY } from './types';",
76
+ "PRIORITY": "export { isLevelEnabled, PRIORITY } from './types';"
77
+ }
78
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "apiSource": "export { createSandbox } from './_runtime.js';\nexport { SandboxConfigurationError, SandboxError, SandboxTimeoutError } from './errors.js';\nexport type { SandboxBridge, SandboxHandle, SandboxMessage, SandboxOptions } from './types.js';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Sandbox — Sandboxed iframe runtime\ndescription: Isolated iframe runtime with a typed state bridge and explicit message trust boundary for safe execution of untrusted HTML — component previews, playgrounds, plugin sandboxes, and more.\npackage: sandbox\ncategory: ui-primitives\nkeywords: [sandbox, iframe, isolation, playground, csp, postmessage, security, components]\nexports:\n [\n createSandbox,\n SandboxConfigurationError,\n SandboxError,\n SandboxTimeoutError,\n SandboxHandle,\n SandboxOptions,\n SandboxBridge,\n SandboxMessage,\n ]\nrelated: [codex, refine]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"sandbox\" />\n\n## Why Sandbox?\n\nRunning untrusted HTML in the main window is unsafe — arbitrary code can access the DOM, cookies, and user data. Sandbox creates an isolated `<iframe sandbox=\"allow-scripts\">` that receives content over a typed state bridge and explicit message trust boundary. The sandbox cannot reach the host page.\n\n```ts\n// Before\ncontainer.innerHTML = untrustedHtml;\n\n// After\nconst sandbox = createSandbox(container);\nawait sandbox.render(untrustedHtml);\n```\n\nCommon use cases:\n\n- **Component previews** — render isolated HTML/CSS examples in documentation or design tools\n- **Code playgrounds** — execute user-provided code with full error forwarding and state injection\n- **Plugin sandboxes** — host third-party or user-authored plugin UI without granting host access\n- **User-generated content** — display untrusted HTML (emails, form output, external widgets) safely\n- **Widget embedding** — wrap third-party widgets with strict CSP and bidirectional messaging\n- **AI-generated UI** — render LLM-produced HTML components with guaranteed isolation\n\n| Feature | Raw `<iframe>` | Sandbox |\n| -------------------------- | -------------------------------------------- | --------------------------------------------- |\n| Bundle size | 0 B (built-in) | <PackageInfo package=\"sandbox\" type=\"size\" /> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Content-Security-Policy | Manual | Auto-generated, strict by default |\n| Managed message protocol | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Typed host state; untrusted inbound details |\n| Error forwarding | <ore-icon name=\"x\" size=\"16\"></ore-icon> | `onerror` + `unhandledrejection` → host |\n| Dispose / `using` | Manual `remove()` | `dispose()` + `[Symbol.dispose]` |\n\n<div class=\"decision-callout\">\n\n**Use Sandbox when** you need to render untrusted or user-provided HTML in the browser with iframe isolation, CSP enforcement, and explicit host/sandbox messaging.\n\n**Consider a raw `<iframe>` when** you only need to embed a known third-party URL — Sandbox is for programmatic `srcdoc` content, not URL-based embedding.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/sandbox\n```\n\n```sh [npm]\nnpm install @vielzeug/sandbox\n```\n\n```sh [yarn]\nyarn add @vielzeug/sandbox\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\n\nconst container = document.getElementById('preview')!;\nconst sandbox = createSandbox<{ theme: 'dark' | 'light' }>(container);\n\nsandbox.onMessage((message) => {\n if (message.type === 'custom') console.log(message.event, message.detail);\n if (message.type === 'error') console.error(message.message);\n if (message.type === 'resize') console.log('height:', message.height);\n});\n\ntry {\n await sandbox.render('<ore-button variant=\"primary\">Click me</ore-button>');\n sandbox.setState({ theme: 'dark' });\n} catch (error) {\n console.error('Sandbox render failed', error);\n} finally {\n sandbox.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createSandbox<State>()` — Creates an isolated `<iframe sandbox=\"allow-scripts\">` with typed outbound state\n- `render(html)` — Creates or replaces the document and resolves when its bridge reports ready\n- `setState(update)` — Pushes one or more typed state values in one message\n- `replaceBody(html)` — Updates streamed body content without resetting head scripts and styles\n- `updateStyle(id, css)` — Patches a named style without replacing the document\n- `SandboxMessage` — Keeps sandbox-controlled custom details typed as `unknown` on the host\n- `SandboxBridge<State, Events>` — Types authored sandbox-side state subscriptions and event emission\n- `readyTimeout` — Rejects blocked document initialization with `SandboxTimeoutError`\n- Strict CSP — Uses `default-src 'none'` and blocks network requests by default\n- Error forwarding — Installs before user scripts and forwards uncaught errors and rejections\n- Stale-message protection — Rejects messages from superseded render generations\n- Disposable — Supports `dispose()`, `disposalSignal`, and `[Symbol.dispose]`\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- [Codex](/codex/) — MCP server with `generate-sandbox-document` and `get-state-bridge-spec` tools; generates document templates for use with Sandbox\n- [Refine](/refine/) — Web component library; renders correctly inside the sandbox via `<script>` injection and `allowedScriptOrigins`\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Sandbox — API Reference\ndescription: API reference for the @vielzeug/sandbox managed iframe runtime, bridge, errors, and testing utilities.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createSandbox()` | Create a managed sandbox iframe | Sync | The iframe is created lazily by `render()` |\n| `SandboxHandle.render()` | Replace the document and await readiness | Async | Injected scripts can trigger a timeout |\n| `SandboxHandle` | Update, observe, and dispose the iframe | Sync methods | State/body updates require a ready document |\n| `SandboxOptions` | Configure metadata, CSP sources, scripts, styles, and timeout | Sync validation | Invalid values throw during creation |\n| `SandboxMessage` | Host-side message union | Sync callback | Custom details are always `unknown` |\n| `SandboxBridge` | Typed sandbox-side `window.__sandbox__` API | Sync | Its generics do not validate host-side input |\n| `SandboxError` | Base class for package errors | Thrown/rejected | Narrow caught values with `instanceof` |\n\n## Package Entry Point\n\n| Import | Exports |\n| --- | --- |\n| `@vielzeug/sandbox` | Runtime, errors, and public types |\n| `@vielzeug/sandbox/testing` | `createSandboxTestHelpers()` |\n\n```ts\nimport {\n createSandbox,\n SandboxConfigurationError,\n SandboxError,\n SandboxTimeoutError,\n} from '@vielzeug/sandbox';\nimport type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n} from '@vielzeug/sandbox';\n```\n\n## Runtime\n\n### `createSandbox<State>(container, options?)`\n\n```ts\nfunction createSandbox<State extends object = Record<string, unknown>>(\n container: HTMLElement,\n options?: SandboxOptions,\n): SandboxHandle<State>;\n```\n\nCreates a managed `<iframe sandbox=\"allow-scripts\">` in `container`. Construction validates options but does not create DOM. The first `render()` creates the iframe with `referrerpolicy=\"no-referrer\"` and a generated CSP.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host element that owns the iframe |\n| `options` | `SandboxOptions` | Optional document and runtime configuration |\n\n**Returns:** `SandboxHandle<State>`.\n\n`State` describes values sent from the trusted host to the sandbox through `setState()`. Ordinary interfaces are supported.\n\n```ts\ninterface PreviewState {\n locale: string;\n theme: 'dark' | 'light';\n}\n\nconst sandbox = createSandbox<PreviewState>(container, {\n styles: { theme: 'body { color-scheme: light; }' },\n title: 'Preview',\n});\n\nawait sandbox.render('<main>Ready</main>');\nsandbox.setState({ locale: 'en', theme: 'dark' });\n```\n\n### `SandboxHandle<State>`\n\n```ts\ninterface SandboxHandle<State extends object = Record<string, unknown>> {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n dispose(): void;\n onMessage(handler: (message: SandboxMessage) => void): () => void;\n render(html: string): Promise<void>;\n replaceBody(html: string): void;\n setState(update: Partial<State>): void;\n updateStyle(id: string, css: string): void;\n [Symbol.dispose](): void;\n}\n```\n\n| Method/property | Description |\n| --- | --- |\n| `render(html)` | Replace the complete document and await readiness |\n| `replaceBody(html)` | Replace body descendants without navigation |\n| `setState(update)` | Push typed host state |\n| `updateStyle(id, css)` | Patch a named style |\n| `onMessage(handler)` | Subscribe to untrusted iframe messages |\n| `disposed` / `disposalSignal` | Observe lifecycle state |\n| `dispose()` / `[Symbol.dispose]()` | Release the iframe and listeners |\n\n#### `render(html)`\n\nReplaces the complete `srcdoc` document. The returned Promise resolves when that document emits its internal ready message and rejects with `SandboxTimeoutError` after `readyTimeout`.\n\nA newer render supersedes and resolves the previous pending render. Disposal also resolves a pending render. Messages from superseded generations are ignored.\n\nThe generated bridge is installed in `<head>` before injected scripts and user HTML, so it can capture script errors during document initialization. Its ready message is sent on `DOMContentLoaded`.\n\n#### `replaceBody(html)`\n\nReplaces `document.body.innerHTML` without navigating. Head scripts, document/window listeners, and named styles survive. Body descendants, descendant listeners, references, form state, and scripts in replacement HTML do not.\n\nCall only after `render()` resolves. The method warns in development and does nothing when no ready document exists.\n\n#### `setState(update)`\n\nSends one object containing one or more state values. Inside the iframe, each key dispatches its own `sandbox:state-update` `CustomEvent` and reaches matching `SandboxBridge.onState()` subscribers.\n\n```ts\nsandbox.setState({ locale: 'en', theme: 'dark' });\n```\n\nCalling before `render()` resolves can lose the update and emits a development warning.\n\n#### `updateStyle(id, css)`\n\nUpdates a style declared in `options.styles`. Before the first render it updates only the render baseline. After readiness it also patches the live `<style>` element. Unknown IDs emit a development warning.\n\n#### `onMessage(handler)`\n\nSubscribes to application messages and returns an idempotent unsubscribe function. Internal ready messages are not forwarded.\n\n```ts\nsandbox.onMessage((message) => {\n if (message.type === 'resize') container.style.height = `${message.height}px`;\n});\n```\n\n#### Lifecycle\n\n`dispose()` removes the iframe, removes the host `message` listener, clears subscribers, resolves a pending render, and aborts `disposalSignal`. It is idempotent. `[Symbol.dispose]()` provides the same behavior for `using` declarations.\n\n## Types\n\n### `SandboxOptions`\n\n```ts\ninterface SandboxOptions {\n allowedFontOrigins?: string[];\n allowedImageOrigins?: string[];\n allowedScriptOrigins?: string[];\n allowedStyleOrigins?: string[];\n lang?: string;\n nonce?: string;\n readyTimeout?: number;\n scripts?: string[];\n styles?: Record<string, string>;\n title?: string;\n}\n```\n\n| Option | Default | Description |\n| --- | --- | --- |\n| `allowedFontOrigins` | `[]` | HTTP(S) origins added to `font-src`; otherwise `'none'` |\n| `allowedImageOrigins` | `[]` | HTTP(S) origins added to `img-src`; `data:` is always included |\n| `allowedScriptOrigins` | `[]` | HTTP(S) origins added to `script-src` |\n| `allowedStyleOrigins` | `[]` | HTTP(S) origins added to `style-src` |\n| `lang` | `'en'` | Basic BCP 47 language tag for `<html lang>` |\n| `nonce` | `undefined` | Base64/base64url-style nonce for the bridge script and CSP |\n| `readyTimeout` | `5000` | Positive finite timeout in milliseconds, at most `2^31 - 1` |\n| `scripts` | `[]` | Absolute HTTP(S) scripts injected before user HTML |\n| `styles` | `{}` | Named style blocks; IDs start with a letter and contain letters, digits, `_`, or `-` |\n| `title` | `''` | Escaped document title |\n\nAllowed origins reject paths, credentials, query strings, fragments, unsupported schemes, and CSP syntax. Script URLs must be absolute HTTP(S) URLs.\n\n### `SandboxMessage`\n\n```ts\ntype SandboxMessage =\n | { detail: unknown; event: string; type: 'custom' }\n | { message: string; stack?: string; type: 'error' }\n | { height: number; type: 'resize' };\n```\n\nAll fields originate in sandboxed code and are untrusted. Validate custom `detail`, and render text through escaping APIs rather than HTML injection.\n\n### `SandboxBridge<State, Events>`\n\nThe generated document exposes `window.__sandbox__`:\n\n```ts\ninterface SandboxBridge<\n State extends object = Record<string, unknown>,\n Events extends object = Record<string, unknown>,\n> {\n emit<K extends keyof Events & string>(\n event: K,\n ...detail: undefined extends Events[K] ? [detail?: Events[K]] : [detail: Events[K]]\n ): void;\n onState<K extends keyof State & string>(\n key: K,\n handler: (value: State[K]) => void,\n ): () => void;\n}\n```\n\nUse an ambient declaration in authored sandbox-side TypeScript:\n\n```ts\ninterface State {\n theme: 'dark' | 'light';\n}\n\ninterface Events {\n saved: { id: string };\n}\n\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge<State, Events>;\n}\n```\n\nThese generics check authored sandbox code only. They do not make host-side messages trustworthy.\n\n## Errors\n\n### `SandboxError`\n\nBase class for package errors. Accepts `opts?: ErrorOptions` for cause chaining.\n\n### `SandboxConfigurationError`\n\nThrown synchronously by `createSandbox()` when an option cannot produce a valid document or CSP.\n\n### `SandboxTimeoutError`\n\nRejected by `render()` when no ready message arrives before `readyTimeout`. An injected script may have blocked document initialization.\n\n## Testing\n\n```ts\nimport { createSandboxTestHelpers } from '@vielzeug/sandbox/testing';\n```\n\n`createSandboxTestHelpers(container)` returns `fireReady()`, `fireCustom()`, `fireError()`, and `fireResize()` helpers. Call `render()` first so the helper can read the iframe channel and generation metadata.\n",
6
+ "usage": "---\ntitle: Sandbox — Usage Guide\ndescription: Render untrusted HTML, push typed state, receive sandbox messages, update live content, and manage iframe lifecycle.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a sandbox, subscribe before rendering, await readiness, and dispose it when finished.\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\n\nconst container = document.getElementById('preview')!;\nconst sandbox = createSandbox(container);\n\nsandbox.onMessage((message) => {\n if (message.type === 'error') console.error(message.message);\n if (message.type === 'resize') container.style.height = `${message.height}px`;\n});\n\ntry {\n await sandbox.render('<main><h1>Preview</h1></main>');\n} finally {\n sandbox.dispose();\n}\n```\n\nThe iframe is created by the first `render()` call. Each render creates a fresh document and resolves after its generated bridge reports ready.\n\n## Rendering HTML\n\nUse `render()` when scripts, styles, globals, and DOM state should reset together.\n\n```ts\nawait sandbox.render(`\n <style>body { font-family: system-ui; }</style>\n <button id=\"save\">Save</button>\n <script>\n document.querySelector('#save').addEventListener('click', () => {\n window.__sandbox__.emit('save');\n });\n </script>\n`);\n```\n\nA newer render supersedes a pending one. Messages from the old document are ignored through the per-render generation marker.\n\n## Pushing Typed State\n\nPass a state interface to `createSandbox()` and send partial updates with one object.\n\n```ts\ninterface PreviewState {\n locale: string;\n theme: 'dark' | 'light';\n user: { name: string };\n}\n\nconst sandbox = createSandbox<PreviewState>(container);\nawait sandbox.render('<main id=\"app\"></main>');\n\nsandbox.setState({\n locale: 'en',\n theme: 'dark',\n user: { name: 'Ada' },\n});\n```\n\nSubscribe inside the iframe through the typed bridge.\n\n```ts\ninterface PreviewState {\n locale: string;\n theme: 'dark' | 'light';\n}\n\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge<PreviewState>;\n}\n\nwindow.__sandbox__.onState('theme', (theme) => {\n document.documentElement.dataset.theme = theme;\n});\n```\n\nCall `setState()` only after `render()` resolves. An early update can arrive before the generated bridge installs its listener and is dropped with a development warning.\n\n## Receiving Sandbox Messages\n\nSubscribe with `onMessage()` before rendering. The unsubscribe function detaches only that handler.\n\n```ts\nconst unsubscribe = sandbox.onMessage((message) => {\n if (message.type === 'custom' && message.event === 'saved') {\n if (isSavedDetail(message.detail)) persist(message.detail.id);\n }\n});\n\nfunction isSavedDetail(value: unknown): value is { id: string } {\n return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';\n}\n```\n\nCustom event details remain `unknown` on the host. The iframe is isolated, but its code still controls message payloads.\n\nUse `SandboxBridge<State, Events>` only to check authored sandbox-side calls.\n\n```ts\ninterface Events {\n saved: { id: string };\n}\n\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge<Record<string, never>, Events>;\n}\n\nwindow.__sandbox__.emit('saved', { id: '42' });\n```\n\n## Updating Live Content\n\n### Replace the body\n\nUse `replaceBody()` when streamed HTML should preserve head scripts, styles, and global listeners.\n\n```ts\nawait sandbox.render('<main>Waiting…</main>');\n\nlet html = '';\nfor await (const chunk of stream) {\n html += chunk;\n sandbox.replaceBody(html);\n}\n```\n\nReplacement uses `document.body.innerHTML`. Scripts in replacement HTML do not execute, and body descendant state and listeners are discarded.\n\n### Update a named style\n\nDeclare styles at construction and patch them without replacing the document.\n\n```ts\nconst sandbox = createSandbox(container, {\n styles: { theme: 'body { color-scheme: light; }' },\n});\n\nawait sandbox.render('<main>Preview</main>');\nsandbox.updateStyle('theme', 'body { color-scheme: dark; }');\n```\n\nCalling `updateStyle()` before the first render updates the render baseline. Unknown IDs emit a development warning.\n\n## Injecting Scripts and Configuring CSP\n\nList absolute external scripts in `scripts`. Their origins are added to `script-src` automatically.\n\n```ts\nconst sandbox = createSandbox(container, {\n allowedFontOrigins: ['https://fonts.gstatic.com'],\n allowedStyleOrigins: ['https://fonts.googleapis.com'],\n scripts: ['https://cdn.example.com/widget.js'],\n});\n```\n\nNetwork requests remain blocked by `connect-src 'none'`. The package does not expose arbitrary CSP directives.\n\nA nonce authorizes the generated bridge while preventing un-nonced inline user scripts in CSP Level 3 browsers.\n\n```ts\nconst sandbox = createSandbox(container, {\n nonce: crypto.randomUUID(),\n});\n```\n\n## Handling Errors and Timeouts\n\n`render()` rejects if the document does not become ready before `readyTimeout`.\n\n```ts\nimport { SandboxError, SandboxTimeoutError } from '@vielzeug/sandbox';\n\ntry {\n await sandbox.render(html);\n} catch (error) {\n if (error instanceof SandboxTimeoutError) {\n showError('A script blocked preview initialization.');\n } else if (error instanceof SandboxError) {\n showError(error.message);\n }\n}\n```\n\nUncaught iframe errors and unhandled rejections arrive through `onMessage()` as untrusted `error` messages.\n\n## Disposal\n\nUse `dispose()` or a `using` declaration to remove the iframe and host listeners.\n\n```ts\nusing sandbox = createSandbox(container);\nawait sandbox.render('<p>Temporary preview</p>');\n```\n\nTie other asynchronous work to `disposalSignal`.\n\n```ts\nconst response = await fetch('/preview-data', {\n signal: sandbox.disposalSignal,\n});\n```\n\n## Testing\n\nUse the testing subpath to complete `render()` and simulate messages in jsdom.\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\nimport { createSandboxTestHelpers } from '@vielzeug/sandbox/testing';\nimport { expect, it } from 'vitest';\n\nit('receives resize messages', async () => {\n const container = document.createElement('div');\n const sandbox = createSandbox(container);\n const helpers = createSandboxTestHelpers(container);\n const heights: number[] = [];\n\n sandbox.onMessage((message) => {\n if (message.type === 'resize') heights.push(message.height);\n });\n\n const render = sandbox.render('<p>Preview</p>');\n helpers.fireReady();\n await render;\n helpers.fireResize(240);\n\n expect(heights).toEqual([240]);\n sandbox.dispose();\n});\n```\n\n## Framework Integration\n\nOwn one sandbox per mounted component and dispose it during unmount.\n\n::: code-group\n\n```tsx [React]\nimport { createSandbox, type SandboxHandle } from '@vielzeug/sandbox';\nimport { useEffect, useRef } from 'react';\n\nexport function Preview({ html }: { html: string }) {\n const container = useRef<HTMLDivElement>(null);\n const sandbox = useRef<SandboxHandle>();\n\n useEffect(() => {\n sandbox.current = createSandbox(container.current!);\n return () => sandbox.current?.dispose();\n }, []);\n\n useEffect(() => {\n void sandbox.current?.render(html).catch(console.error);\n }, [html]);\n\n return <div ref={container} />;\n}\n```\n\n```vue [Vue]\n<script setup lang=\"ts\">\nimport { createSandbox, type SandboxHandle } from '@vielzeug/sandbox';\nimport { onMounted, onUnmounted, ref, watch } from 'vue';\n\nconst props = defineProps<{ html: string }>();\nconst container = ref<HTMLElement>();\nlet sandbox: SandboxHandle | undefined;\n\nonMounted(() => {\n sandbox = createSandbox(container.value!);\n void sandbox.render(props.html).catch(console.error);\n});\nwatch(() => props.html, (html) => void sandbox?.render(html).catch(console.error));\nonUnmounted(() => sandbox?.dispose());\n</script>\n\n<template><div ref=\"container\" /></template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createSandbox, type SandboxHandle } from '@vielzeug/sandbox';\n import { onMount } from 'svelte';\n\n export let html: string;\n let container: HTMLElement;\n let sandbox: SandboxHandle | undefined;\n\n onMount(() => {\n sandbox = createSandbox(container);\n return () => sandbox?.dispose();\n });\n\n $: if (sandbox) void sandbox.render(html).catch(console.error);\n</script>\n\n<div bind:this={container}></div>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Sandbox to isolate generated markup that loads browser bundles of DOM-output packages such as Refine or Ore. Use Rune in the host to record validated message details; do not pass untrusted payloads directly to structured logs.\n\n## Best Practices\n\n- Await `render()` before sending state or replacing body content.\n- Narrow every custom message detail at the host boundary.\n- Prefer `setState()` for data changes and `updateStyle()` for CSS changes.\n- Use `render()` when scripts or global state must reset.\n- Handle `SandboxTimeoutError` at every non-test render boundary.\n- Dispose one sandbox with each owning component.\n- Tie related asynchronous work to `disposalSignal`.\n- Keep network access blocked unless the application requires a different isolation model.\n",
7
+ "examples": "---\ntitle: Sandbox — Examples\ndescription: Recipes for common Sandbox use cases — component previews, user script sandboxes, and embedded widgets.\n---\n\n## Examples\n\n- [Component Preview](./examples/component-preview.md)\n- [User Script Sandbox](./examples/user-script-sandbox.md)\n- [Embedded Widget](./examples/embedded-widget.md)\n- [AI UI Renderer](./examples/ai-ui-renderer.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "error-normalize",
12
+ "code": "import { SandboxError } from '@vielzeug/sandbox'\n\n// Normalize any caught error into a typed SandboxError, preserving the original cause\nfunction toSandboxError(err) {\n if (err instanceof SandboxError) return err\n const message = err instanceof Error ? err.message : String(err)\n return new SandboxError(`sandbox operation failed: ${message}`, { cause: err })\n}\n\ntry {\n JSON.parse('{ not valid json')\n} catch (parseError) {\n const sandboxError = toSandboxError(parseError)\n console.log('Wrapped error name:', sandboxError.name)\n console.log('Wrapped error message:', sandboxError.message)\n console.log('Original cause preserved:', sandboxError.cause === parseError)\n console.log('instanceof SandboxError:', sandboxError instanceof SandboxError)\n console.log('instanceof Error:', sandboxError instanceof Error)\n}\n\n// Custom subclasses are still recognised by instanceof\nclass SandboxTimeoutError extends SandboxError {}\nconst timeoutError = new SandboxTimeoutError('render() did not resolve in time')\nconsole.log('\\nSubclass name:', timeoutError.name)\nconsole.log('Subclass instanceof SandboxError:', timeoutError instanceof SandboxError)\nconsole.log('Plain Error rejected:', !(new Error('nope') instanceof SandboxError))",
13
+ "name": "Normalize Errors with SandboxError"
14
+ }
15
+ ],
16
+ "typeSignatures": {
17
+ "createSandbox": "export { createSandbox } from './_runtime.js';",
18
+ "SandboxConfigurationError": "export { SandboxConfigurationError, SandboxError, SandboxTimeoutError } from './errors.js';",
19
+ "SandboxError": "export { SandboxConfigurationError, SandboxError, SandboxTimeoutError } from './errors.js';",
20
+ "SandboxTimeoutError": "export { SandboxConfigurationError, SandboxError, SandboxTimeoutError } from './errors.js';",
21
+ "SandboxBridge": "export type { SandboxBridge, SandboxHandle, SandboxMessage, SandboxOptions } from './types.js';",
22
+ "SandboxHandle": "export type { SandboxBridge, SandboxHandle, SandboxMessage, SandboxOptions } from './types.js';",
23
+ "SandboxMessage": "export type { SandboxBridge, SandboxHandle, SandboxMessage, SandboxOptions } from './types.js';",
24
+ "SandboxOptions": "export type { SandboxBridge, SandboxHandle, SandboxMessage, SandboxOptions } from './types.js';"
25
+ }
26
+ }