@vielzeug/codex 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/data/catalog.json +0 -1885
- package/data/llms-full.txt +0 -31974
- package/data/llms.txt +0 -45
- package/data/manifest.json +0 -8
- package/data/packages/arsenal.json +0 -210
- package/data/packages/assay.json +0 -39
- package/data/packages/clockwork.json +0 -67
- package/data/packages/codex.json +0 -43
- package/data/packages/coins.json +0 -102
- package/data/packages/conduit.json +0 -60
- package/data/packages/courier.json +0 -59
- package/data/packages/dnd.json +0 -77
- package/data/packages/familiar.json +0 -40
- package/data/packages/flux.json +0 -93
- package/data/packages/focus.json +0 -37
- package/data/packages/forge.json +0 -83
- package/data/packages/gesture.json +0 -25
- package/data/packages/herald.json +0 -108
- package/data/packages/illusionist.json +0 -132
- package/data/packages/keymap.json +0 -60
- package/data/packages/ledger.json +0 -57
- package/data/packages/lingua.json +0 -68
- package/data/packages/necromancer.json +0 -50
- package/data/packages/orbit.json +0 -99
- package/data/packages/ore.json +0 -68
- package/data/packages/postmaster.json +0 -45
- package/data/packages/prism.json +0 -66
- package/data/packages/pulse.json +0 -70
- package/data/packages/refine.json +0 -12
- package/data/packages/ripple.json +0 -83
- package/data/packages/rune.json +0 -79
- package/data/packages/sandbox.json +0 -40
- package/data/packages/scout.json +0 -61
- package/data/packages/scroll.json +0 -109
- package/data/packages/sentinel.json +0 -35
- package/data/packages/sourcerer.json +0 -73
- package/data/packages/spell.json +0 -133
- package/data/packages/tempo.json +0 -81
- package/data/packages/vault.json +0 -79
- package/data/packages/ward.json +0 -114
- package/data/packages/wayfinder.json +0 -110
- package/data/refine.json +0 -11887
- package/data/search.json +0 -1578
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"apiSource": "export type { AsyncState, Resource, ResourceOptions } from './_async';\nexport { createRipple, type Ripple } from './_default';\nexport type { WatchOptions } from './_watch';\nexport {\n RippleComputedCycleError,\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 ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\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;\n",
|
|
3
|
-
"docs": {
|
|
4
|
-
"index": "---\ntitle: Ripple — Reactive graphs\ndescription: Framework-agnostic signals, derived values, effects, scopes, watchers, and async resources.\npackage: ripple\ncategory: state\nkeywords: [reactive, signals, computed, effects, graph, scope, batch, watch, resource, async]\nrelated: [ore, clockwork, ledger]\nexports: [createRipple, signal, computed, effect, batch, createScope, untrack, watch, resource, 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 resources 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- `resource()` loads async values with stale-work cancellation.\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 resources.\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| `resource()` | Load async source | Async | Read dependencies in source callback |\n| `isReactive()` | Test `Readable` identity | Sync | Does not test arbitrary objects |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ripple` | All primitives, types, and errors — signals, computed, effects, scopes, watch, resource, 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, observer, 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.onError` | `(error, context) => void` | Receives effect, cleanup, listener, or observer failures. |\n| `options.observer` | `ReactiveObserver` | Receives graph events. |\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 whether a value is a Ripple-created readable node, including `Resource`. Recognition works across duplicated Ripple module graphs.\n\n**Returns:** `true` for a Ripple `Signal`, computed value, or `Resource`; otherwise `false`.\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\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`.\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.\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## Watch and Resources\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.\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### `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\nTracks `source`, aborts stale loader work, and exposes `AsyncState<Value>`. Source and loader failures become `status: 'error'` state; handle them from `resource.value` rather than `RippleOptions.onError`, which is reserved for runtime callback, cleanup, listener, and observer failures.\n\n**Returns:** `Resource<Value>`.\n\n**Example:**\n\n```ts\nimport { resource, signal } from '@vielzeug/ripple';\n\nconst userId = signal('42');\nconst user = resource(() => userId.value, async (id) => ({ id }));\n\nif (user.value.status === 'error') console.error(user.value.error);\nuser.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 };\ntype ResourceOptions = { name?: string };\n\ntype ReactiveEvent =\n | { readonly kind: 'compute'; readonly name?: string }\n | { readonly kind: 'effect'; readonly name?: string }\n | { readonly kind: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }\n | { readonly kind: 'dispose'; readonly name?: string; readonly node: 'effect' | 'scope' };\n\ntype ReactiveObserver = (event: ReactiveEvent) => void;\ntype ReactiveErrorContext = { readonly kind: 'cleanup' | 'effect' | 'listener' | 'observer'; readonly name?: string };\ntype RippleOptions = { observer?: ReactiveObserver; onError?: (error: unknown, context: ReactiveErrorContext) => void };\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 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 Resource<T> extends Readable<AsyncState<T>>, Disposable { reload(): void }\n\ninterface Ripple {\n batch<T>(fn: () => T): T;\n computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n createScope(name?: string): Scope;\n dispose(): void;\n readonly disposed: boolean;\n effect(callback: () => Cleanup | undefined, options?: EffectOptions): EffectHandle;\n resource<Source, Value>(source: () => Source, loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>, options?: ResourceOptions): Resource<Value>;\n signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\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| `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({\n onError(error, context) {\n console.log(context.kind, error);\n },\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\n## Watch Selected Values\n\nUse `watch()` for one selected output. Use `effect()` when every reactive read in the callback 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## Async Data\n\n`resource()` captures source dependencies synchronously and passes a cancellation signal to the loader.\n\n```ts\nconst userId = ripple.signal('42');\nconst user = ripple.resource(\n () => userId.value,\n async (id, { signal }) => {\n const response = await fetch(`/users/${id}`, { signal });\n if (!response.ok) throw new Error(`Request failed: ${response.status}`);\n\n return response.json() as Promise<{ id: string; name: string }>;\n },\n);\n\nif (user.value.status === 'success') console.log(user.value.value.name);\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\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 and resource work 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 `onError` 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 request, test, and feature graphs.\n- Batch related synchronous writes.\n- Use `watch()` only for selected source transitions.\n- Read dependencies in a resource source, not its loader.\n- Use `onError` for runtime callback, cleanup, listener, and observer failures; handle resource source and loader failures through `resource.value.status === 'error'`.\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- [Immutable State](./examples/immutable-store.md)\n- [Isolated Graph](./examples/isolated-runtime.md)\n- [Async Resource](./examples/async-resource.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
|
-
"WatchOptions": "export type { WatchOptions } from './_watch';",
|
|
53
|
-
"RippleComputedCycleError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
|
|
54
|
-
"RippleDisposedRuntimeError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
|
|
55
|
-
"RippleDisposedScopeError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
|
|
56
|
-
"RippleError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
|
|
57
|
-
"RippleInfiniteLoopError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
|
|
58
|
-
"isReactive": "export { isReactive } from './runtime';",
|
|
59
|
-
"Cleanup": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
60
|
-
"ComputedOptions": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
61
|
-
"Disposable": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
62
|
-
"EffectHandle": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
63
|
-
"EffectOptions": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
64
|
-
"Equality": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
65
|
-
"ReactiveErrorContext": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
66
|
-
"ReactiveEvent": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
67
|
-
"ReactiveObserver": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
68
|
-
"Readable": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
69
|
-
"RippleOptions": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
70
|
-
"Scope": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
71
|
-
"Signal": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
72
|
-
"SignalOptions": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
73
|
-
"Unsubscribe": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
|
|
74
|
-
"signal": "export const signal = defaultRipple.signal;",
|
|
75
|
-
"computed": "export const computed = defaultRipple.computed;",
|
|
76
|
-
"effect": "export const effect = defaultRipple.effect;",
|
|
77
|
-
"batch": "export const batch = defaultRipple.batch;",
|
|
78
|
-
"createScope": "export const createScope = defaultRipple.createScope;",
|
|
79
|
-
"untrack": "export const untrack = defaultRipple.untrack;",
|
|
80
|
-
"watch": "export const watch = defaultRipple.watch;",
|
|
81
|
-
"resource": "export const resource = defaultRipple.resource;"
|
|
82
|
-
}
|
|
83
|
-
}
|
package/data/packages/rune.json
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"apiSource": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';\nexport { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';\nexport type { LazyBinding } from './lazy';\nexport { lazy } from './lazy';\nexport { createLogger, defaultLogger } from './logger';\nexport { batchTransport, jsonTransport, pipe, 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 PipeOptions,\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 defaultLogger,\n consoleTransport,\n remoteTransport,\n jsonTransport,\n batchTransport,\n sampleTransport,\n redactTransport,\n pipe,\n lazy,\n isLevelEnabled,\n resolveTheme,\n DEFAULT_THEME,\n PRIORITY,\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}`);\nfetch('/api/logs', { body: JSON.stringify({ level: 'error', path }), method: 'POST' });\n\n// After — Rune\nimport { consoleTransport, createLogger, remoteTransport } from '@vielzeug/rune';\n\nconst api = createLogger({\n namespace: 'api',\n transports: [\n consoleTransport({ level: 'debug' }),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n ],\n});\n\napi.info({ method: 'GET', path }, 'request');\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 { batchTransport, consoleTransport, createLogger, lazy, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'server',\n transports: [\n consoleTransport({ timestamp: true }),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n ],\n});\n\nconst requestLog = log.withBindings({\n diagnostics: lazy(() => ({ queueDepth: 0 })),\n requestId: 'abc-123',\n});\n\nrequestLog.info({ method: 'GET', path: '/users' }, 'request');\nconst users = await requestLog.time('load users', () => Promise.resolve(['user-1']));\nconsole.log(users);\n\nconst batch = batchTransport({ onFlush: (entries) => console.debug('batch', entries) });\nconst bufferedLog = createLogger({ transports: [batch.transport] });\n\nbufferedLog.info('queued for delivery');\nawait batch.dispose();\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- Three call forms: `log.info('msg')`, `log.error(err, { id }, 'msg')` (Error-first), or `log.info({ key: 'val' }, 'msg')` — Error-first form auto-serializes to `data.err`\n- `Error` values in context fields are also 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- Middleware pipeline via `use(fn)` — transform or filter entries before transport dispatch\n- Pluggable transport pipeline: `consoleTransport`, `remoteTransport`, `jsonTransport`, `batchTransport`, `sampleTransport`, `redactTransport`\n- Fan-out via `pipe()` — dispatch to multiple transports independently, fault-tolerant\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| `defaultLogger` | Pre-created default logger singleton | — | Shared instance — use `child()` or `withBindings()` to scope |\n| `lazy(fn)` | Defer a binding value past the level check | Sync | Factory runs on every emit, not once |\n| `pipe()` | Fan-out dispatcher to multiple transports | Sync | Errors in one transport don't propagate to others |\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| `remoteTransport()` | Async HTTP/webhook delivery | Async | Handler errors are swallowed to `console.warn` |\n| `jsonTransport()` | NDJSON to stdout or a custom sink | Sync | `process.stdout` is unavailable in browsers |\n| `batchTransport()` | Buffered batch delivery with flush interval | Async | Await `.dispose()` and handle rejected delivery |\n| `sampleTransport()` | Probabilistic entry forwarding | Sync | `rate: 1` forwards all entries; `rate: 0` forwards none |\n| `redactTransport()` | Sensitive field stripping before forwarding | Sync | Place this closest to the remote transport, not console |\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 — transport/middleware fault isolation:** if a transport or middleware function throws, the logger catches it, reports it via a dev-only warning, and continues — a single misbehaving transport can never crash the caller of `log.info()`/etc., and sibling transports still receive the entry. A throwing middleware drops just that one entry.\n\n**Returns:** `Logger`\n\n**Example:**\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\nimport { consoleTransport, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({ logLevel: 'warn', namespace: 'app' });\n\nconst serverLog = createLogger({\n namespace: 'server',\n transports: [\n consoleTransport(),\n remoteTransport({\n handler: async (_type, data) => {\n await fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' });\n },\n level: 'error',\n }),\n ],\n});\n```\n\n## defaultLogger\n\n`defaultLogger` is the pre-created default logger (`createLogger()` called once at module load).\n\nUse it as a quick-start singleton or create a child for module-level use:\n\n```ts\nimport { defaultLogger } from '@vielzeug/rune';\n\nconst log = defaultLogger.child({ namespace: 'app.worker' });\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 share the same signature:\n\n```ts\nlog.debug / info / warn / error / fatal(message: string): void\nlog.debug / info / warn / error / fatal(error: Error, message?: string): void\nlog.debug / info / warn / error / fatal(error: Error, context: Bindings, message?: string): void\nlog.debug / info / warn / error / fatal(context: Bindings, message?: string): void\n```\n\nArgument rules:\n\n- String-only calls accept a single message argument.\n- **Error-first form:** pass an `Error` as the first argument — it is auto-serialized to `{ message, name, stack }` under the `err` key. Optionally follow with a `Bindings` object and/or a message string.\n- Context object comes first when providing structured data without a top-level Error. `Error` values inside the context object are also auto-serialized to `{ message, name, stack }`.\n\n```ts\nlog.error(err, 'request failed'); // err auto-serialized to data.err\nlog.error(err, { requestId }, 'request failed'); // err + context + message\nlog.error({ err: new Error('boom') }, 'failed'); // Error nested in context object\n```\n\n### Composition\n\n| Method | Returns | What it does |\n| ---------------------- | -------- | ----------------------------------------------------------------- |\n| `child(overrides?)` | `Logger` | Clones config, applies overrides, inherits bindings |\n| `withBindings(fields)` | `Logger` | Pins fields to every subsequent call, returns a new child logger |\n| `use(middleware)` | `Logger` | Appends a middleware function to the pipeline, returns new 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. Does **not** auto-dispose batch transports — hold a reference and call `batchTransport.dispose()` on shutdown. Idempotent. |\n\n### Properties\n\n| Property | Type | Description |\n| ------------------ | -------------------------- | ------------------------------------------------------------------ |\n| `logLevel` | `LogLevel` | Active log level threshold |\n| `namespace` | `string` | Effective namespace string |\n| `middleware` | `readonly LogMiddleware[]` | Middleware pipeline snapshot |\n| `transports` | `readonly Transport[]` | Transport 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### remoteTransport(options)\n\n```ts\nremoteTransport(options: RemoteTransportOptions): Transport\n```\n\nForwards entries asynchronously to a remote handler. Fire-and-forget — handler errors are swallowed to `console.warn` and never propagate to the caller.\n\n| Option | Type | Default | Description |\n| --------- | ------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `handler` | `(type: LogType, data: RemoteLogData) => void` | — | Required. Receives each forwarded entry |\n| `level` | `LogLevel` | `'debug'` | Minimum level to forward |\n| `env` | `'production' \\| 'development'` | auto-detected | Override the runtime environment marker |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | — | Called when the handler throws or rejects. Default: a dev-only `console.warn`. Silent in production — provide an explicit handler for production observability. |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n remoteTransport({\n handler: async (_type, data) => {\n await fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' });\n },\n level: 'error',\n }),\n ],\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({ path: '/users', status: 200 }, 'request');\n// {\"path\":\"/users\",\"status\":200,\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"msg\":\"request\"}\n```\n\n### batchTransport(options)\n\n```ts\nbatchTransport(options: BatchTransportOptions): BatchHandle\n```\n\nBuffers entries and delivers them in order. Flushes when the buffer reaches `maxSize` or after `interval` elapses; `flush()` and `dispose()` wait for accepted batch delivery.\n\n| Option | Type | Default | Description |\n| -------------- | ------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------- |\n| `onFlush` | `(entries: LogEntry[]) => void \\| Promise<void>` | — | Required. Receives each batch; implement retry here when successful retry must fulfill drain |\n| `onFlushError` | `(entries: LogEntry[], error: unknown) => void` | — | Observes delivery failure; matching `flush()` or later `dispose()` rejects |\n| `level` | `LogLevel` | `'debug'` | Minimum level to buffer |\n| `interval` | `number` | `5000` | Finite interval in milliseconds greater than zero |\n| `maxSize` | `number` | `50` | Finite positive integer batch size before early flush |\n| `maxBuffer` | `number` | unbounded | Finite non-negative integer hard cap; oldest entries drop when exceeded |\n\nReturns a `BatchHandle` with:\n\n- `.transport` — the `Transport` function to pass to `createLogger({ transports: [handle.transport] })`.\n- `.flush()` — immediately send buffered entries and resolve after delivery; rejects when delivery fails.\n- `.dispose()` — stop the interval, reject new entries, and settle after every accepted batch completes. Rejects if any automatic or final delivery fails. Idempotent.\n- `.disposed` — `true` when disposal starts.\n- `[Symbol.asyncDispose]()` — delegates to `.dispose()`. Enables `await using` declarations.\n\nAfter `dispose()`, the transport becomes inert: new entries are silently dropped.\n\n**Returns:** `BatchHandle`\n\n**Example:**\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n interval: 10_000,\n maxSize: 100,\n onFlush: (entries) => console.debug('batch', entries),\n});\n\nconst log = createLogger({ transports: [batch.transport] });\n\nasync function shutdown() {\n await batch.dispose();\n}\n```\n\n### sampleTransport(options)\n\n```ts\nsampleTransport(options: SampleTransportOptions): Transport\n```\n\nProbabilistically forwards entries to a downstream transport.\n\n| Option | Type | Default | Description |\n| ----------- | ----------- | --------- | ---------------------------------------------- |\n| `rate` | `number` | — | Required finite fraction of entries to forward (0–1) |\n| `transport` | `Transport` | — | Required. Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level to sample |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, remoteTransport, sampleTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n sampleTransport({\n rate: 0.1,\n transport: remoteTransport({ handler: (_type, data) => console.debug('sampled log', data) }),\n }),\n ],\n});\n```\n\n### redactTransport(options)\n\n```ts\nredactTransport(options: RedactTransportOptions): Transport\n```\n\nStrips sensitive fields from `bindings` and `context` before forwarding. Redaction is applied recursively at any depth (up to 20 levels).\n\n::: warning Key matching\n`keys` matches **exact field names** at any nesting depth. Dot-path notation (e.g. `'user.password'`) is **not** supported — use `'password'` to redact every field named `password` regardless of nesting.\n:::\n\n| Option | Type | Default | Description |\n| ------------- | ----------- | -------------- | ------------------------------- |\n| `keys` | `string[]` | — | Required. Field names to redact |\n| `maxDepth` | `number` | `20` | Finite non-negative integer max nesting depth. Fields deeper than this are not redacted. |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement value |\n| `transport` | `Transport` | — | Required. Downstream transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, redactTransport, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n redactTransport({\n keys: ['password', 'token', 'ssn'],\n transport: remoteTransport({ handler: (_type, data) => console.debug('redacted log', data) }),\n }),\n ],\n});\n```\n\n### pipe(...transports) / pipe(options, ...transports)\n\n```ts\npipe(...transports: Transport[]): Transport\npipe(options: PipeOptions, ...transports: Transport[]): Transport\n```\n\nDispatches each `LogEntry` to every transport in the list independently. An error thrown by one transport does not stop the others. Use in place of separate array entries when you want fault isolation or a shared error observer.\n\n`pipe()` with no arguments creates a valid no-op transport — useful for conditional pipeline construction: `pipe(condition ? remoteTransport(opts) : undefined!)` pattern, or simply as a placeholder during development.\n\n| Option | Type | Description |\n| --------- | ------------------------------------------- | --------------------------------------------------------- |\n| `onError` | `(error: unknown, entry: LogEntry) => void` | Called with the error and entry when any transport throws |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger, pipe, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n pipe(\n { onError: (error) => console.warn('transport error', error) },\n consoleTransport(),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n ),\n ],\n});\n```\n\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/middleware 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### RemoteLogData\n\nPayload shape delivered to `RemoteTransportOptions.handler`:\n\n| Field | Type | Description |\n| ----------- | ------------------------------- | ----------------------------------------- |\n| `data` | `Bindings?` | Merged structured data (omitted if empty) |\n| `env` | `'production' \\| 'development'` | Runtime env marker |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string?` | Effective namespace |\n| `timestamp` | `string` | Full ISO timestamp |\n\n### PipeOptions\n\n| Field | Type | Description |\n| --------- | ------------------------------------------- | ----------------------------------------------------- |\n| `onError` | `(error: unknown, entry: LogEntry) => void` | Called when a transport in the pipe throws or rejects |\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| `bindings` | `Bindings?` | `{}` | Initial pinned bindings |\n| `middleware` | `LogMiddleware[]?` | `[]` | Entry transform/filter chain |\n\n### LogMethod\n\n```ts\ntype LogMethod = {\n (message: string): void;\n (error: Error, message?: string): void;\n (error: Error, context: Bindings, message?: string): void;\n (context: Bindings, message?: string): void;\n};\n```\n\nEvery log-level method uses this signature. Three call forms are supported:\n\n- **String-only:** `log.info('message')`\n- **Error-first:** `log.error(err, { requestId }, 'failed')` — `Error` is auto-serialized to `{ message, name, stack }` under `data.err`. Optionally follow with a `Bindings` object and/or a message string.\n- **Context-first:** `log.info({ key: 'value' }, 'message')` — structured context object, optional message. `Error` values nested inside the context are also auto-serialized.\n\n### LogMiddleware\n\n```ts\ntype LogMiddleware = (entry: LogEntry) => LogEntry | null;\n```\n\nMiddleware functions intercept entries before they reach transports. Return the (optionally mutated) entry to continue, or return `null` to drop the entry. Added via `use(fn)` or `RuneOptions.middleware`.\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### BatchHandle\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```\n\nReturned by `batchTransport()`. Pass `handle.transport` to `createLogger({ transports })`; await `handle.dispose()` during graceful shutdown. `disposed` is `true` when disposal starts.\n\n### Logger\n\nThe full interface returned by `createLogger()` and `defaultLogger`:\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 /** Returns a new child logger with additional pinned bindings. The returned logger is fully independent — disposing it does not affect the parent, and vice versa. */\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### RemoteTransportOptions\n\n| Field | Type | Default | Description |\n| --------- | ------------------------------- | ------------- | --------------------------------------- |\n| `handler` | `(type: LogType, data: RemoteLogData) => void` | — | Required. Receives each forwarded entry |\n| `level` | `LogLevel` | `'debug'` | Minimum level to forward |\n| `env` | `'production' \\| 'development'` | auto-detected | Override the runtime environment marker |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | — | Called when the handler throws |\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### BatchTransportOptions\n\n| Field | Type | Default | Description |\n| -------------- | ------------------------------------------------ | --------- | --------------------------------------------------------- |\n| `onFlush` | `(entries: LogEntry[]) => void \\| Promise<void>` | — | Required. Receives each batch (may be async) |\n| `onFlushError` | `(entries: LogEntry[], error: unknown) => void` | — | Observes delivery failure; matching `flush()` or later `dispose()` rejects |\n| `level` | `LogLevel` | `'debug'` | Minimum level to buffer |\n| `interval` | `number` | `5000` | Finite interval in milliseconds greater than zero |\n| `maxSize` | `number` | `50` | Finite positive integer batch size before early flush |\n| `maxBuffer` | `number` | unbounded | Finite non-negative integer cap; drops oldest entries when exceeded |\n\n### SampleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ----------- | --------- | ---------------------------------------------- |\n| `rate` | `number` | — | Required finite fraction of entries to forward (0–1) |\n| `transport` | `Transport` | — | Required. Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level to sample |\n\n### RedactTransportOptions\n\n| Field | Type | Default | Description |\n| ------------- | ----------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `keys` | `string[]` | — | Required. Field names to redact at any depth |\n| `maxDepth` | `number` | `20` | Finite non-negative integer nesting depth. Fields deeper than this are not redacted — a dev-only warning is emitted when hit. **Security:** the warning is suppressed in production; ensure sensitive fields are not nested beyond this limit. |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement value |\n| `transport` | `Transport` | — | Required. Downstream transport |\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\n`defaultLogger` is the default singleton logger instance. Use `createLogger()` for isolated config.\n\n```ts\nimport { createLogger, defaultLogger } from '@vielzeug/rune';\n\nconst appLog = defaultLogger;\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, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n transports: [\n consoleTransport({ timestamp: true }),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\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| `remoteTransport()` | HTTP/webhook delivery |\n| `jsonTransport()` | NDJSON for server-side log aggregation |\n| `batchTransport()` | Buffered delivery to reduce I/O overhead |\n| `sampleTransport()` | Probabilistic volume reduction |\n| `redactTransport()` | Sensitive field stripping before forwarding |\n| `pipe()` | Fan-out dispatcher to multiple transports |\n\n### Composing Transports\n\nTransport factories are composable wrappers. Chain them to build a pipeline.\n\nWrap a downstream transport to redact fields, sample volume, and batch delivery:\n\n```ts\nimport { batchTransport, consoleTransport, createLogger, redactTransport, sampleTransport } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n interval: 30_000,\n onFlush: (entries) => console.debug('batch', entries),\n});\n\nconst log = createLogger({\n transports: [\n consoleTransport({ level: 'debug' }),\n redactTransport({\n keys: ['password', 'token'],\n transport: sampleTransport({\n rate: 0.1,\n transport: batch.transport,\n }),\n }),\n ],\n});\n\nawait batch.dispose();\n```\n\nUse `pipe()` when every downstream transport must receive an entry despite sibling transport failures:\n\n```ts\nimport { consoleTransport, createLogger, pipe, remoteTransport } from '@vielzeug/rune';\n\nconst fanout = pipe(\n { onError: (error) => console.warn('transport error', error) },\n consoleTransport(),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n);\n\nconst log = createLogger({ transports: [fanout] });\n```\n\n### Batch Transport Lifecycle\n\n`batchTransport` starts an interval timer on first use. Await `.dispose()` during graceful application shutdown to stop the timer and finish delivery for every accepted batch:\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n interval: 10_000,\n maxSize: 100,\n onFlush: (entries) => console.debug('batch', entries),\n});\n\nconst log = createLogger({ transports: [batch.transport] });\n\nasync function shutdown() {\n try {\n await batch.dispose();\n } catch (error) {\n console.error('log delivery failed during shutdown', error);\n throw error;\n }\n}\n```\n\n`batchTransport.dispose()` is idempotent — repeated calls return the same drain promise and never double-flush. It rejects when an accepted batch cannot deliver. `[Symbol.asyncDispose]` is available for `await using` declarations. Do not use a Node `exit` handler: Node cannot await asynchronous cleanup there.\n\n::: warning\n`log.dispose()` silences the logger but does not flush or stop batch transports. Keep a direct batch reference and `await batch.dispose()` during shutdown.\n:::\n\n::: warning\nAfter `log.dispose()`, the logger is silenced — all log calls (`debug`, `info`, `warn`, `error`, `fatal`, `time`, `group`) become no-ops. The `fn` callback in `group()` still executes, but no group header is rendered. This is intentional to prevent logging after application teardown.\n:::\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({ path: '/users', status: 200 }, 'request');\n// Outputs: {\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"path\":\"/users\",\"status\":200,\"msg\":\"request\"}\n```\n\n## Configuration\n\nUse `child()` to derive immutable logger variants.\n\n```ts\nconst AppLog = defaultLogger.child({\n logLevel: 'warn',\n namespace: 'App',\n // transports inherited from defaultLogger 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## Call Signature\n\nAll log methods share a consistent three-form signature:\n\n```ts\nlog.info('message'); // string only\nlog.error(err, 'request failed'); // Error first — auto-serialized to data.err\nlog.error(err, { requestId }, 'request failed'); // Error + context + message\nlog.info({ key: 'value' }, 'message'); // context object first, message second\nlog.error({ err: new Error('boom') }, 'request failed'); // Error nested in context — also auto-serialized\n```\n\n- **Error-first form:** pass an `Error` as the first argument. It is automatically serialized to `{ message, name, stack }` under the `err` key in `data`. Optionally follow with a `Bindings` object and/or a message string. This is the idiomatic form when the Error is the primary subject of the call.\n- **Context-first form:** pass a plain object as the first argument. `Error` values nested inside are also auto-serialized. Optionally follow with a message string.\n- **String-only form:** a single string message, no structured context.\n\nThe per-call context is shallow-merged with `withBindings()` bindings into `entry.data`.\n\n## Logging Methods\n\n```ts\ndefaultLogger.debug('debug details');\ndefaultLogger.info({ port: 3000 }, 'server started');\ndefaultLogger.warn('cache stale');\ndefaultLogger.error({ err: new Error('timeout') }, 'request failed'); // Error auto-serialized in context\ndefaultLogger.fatal({ service: 'db' }, 'terminating'); // above error, use for unrecoverable state\n```\n\nUse `enabled()` to avoid expensive payload construction before the level check:\n\n```ts\nif (defaultLogger.enabled('debug')) {\n defaultLogger.debug({ diagnostics: buildLargePayload() }, 'diagnostics');\n}\n```\n\nOr use `lazy()` to let Rune gate it automatically:\n\n```ts\nconst reqLog = defaultLogger.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 = defaultLogger.child({ namespace: 'api' });\n\nconst reqLog = api.withBindings({ requestId: 'abc-123', userId: 42 });\nreqLog.info('GET /users'); // always includes requestId and userId\nreqLog.warn({ slow: true }, 'query took 2s'); // 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 = defaultLogger.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 = defaultLogger.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 = defaultLogger.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 = defaultLogger.child({ 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\nTo forward timing data to a remote endpoint, include `remoteTransport` in the pipeline — `debug`-level entries will be forwarded at its threshold.\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({ baseUrl: 'https://api.example.com' });\ncourier.use(withLogging({ logger: (message, meta) => log.debug(meta, message) }));\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({ event, payload }, 'dispatched'),\n onError: (err, event) => log.error(err, `handler error in \"${event}\"`),\n});\n```\n\n## Best Practices\n\n- Create one child logger per module boundary using `defaultLogger.child({ namespace: 'module.name' })` or `createLogger('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- Keep remote handlers resilient — network failures should not block app flow.\n- Await `batchTransport.dispose()` during graceful shutdown to drain remaining accepted entries.\n- Use `redactTransport` closest to any remote/persistent transport — never strip before console.\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 { defaultLogger } from '@vielzeug/rune'\n\n// message-only\ndefaultLogger.debug('app starting')\ndefaultLogger.info('ready')\n\n// context-first — structured data before message\ndefaultLogger.info({ port: 3000 }, 'server listening')\ndefaultLogger.warn({ retries: 3 }, 'retrying request')\n\n// Pass Error as a context field — auto-serialized to { message, name, stack }\nconst err = new Error('connection refused')\ndefaultLogger.error({ err }, 'service unavailable')\ndefaultLogger.error({ err, requestId: 'r-001' }, 'request failed')\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 { batchTransport, createLogger } from '@vielzeug/rune';\n\n// Two-arg shorthand: namespace + options\nconst log = createLogger('api', { logLevel: 'debug' });\n\nlog.info('logger created');\nlog.debug({ url: '/health' }, 'request start');\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// batchTransport idempotency — double-dispose does not double-flush\nconst flushed: string[] = [];\nconst batch = batchTransport({\n interval: 60_000,\n onFlush: (entries) => {\n flushed.push(...entries.map((e) => e.message ?? ''));\n },\n});\n\nconst batchLog = createLogger('batch', { transports: [batch.transport] });\nbatchLog.info('entry-1');\nbatchLog.warn('entry-2');\n\nawait batch.dispose(); // flushes once and waits for delivery\nawait batch.dispose(); // same settled promise — no double flush\n\nconsole.log('flushed messages:', flushed);\n",
|
|
28
|
-
"name": "Logger Lifecycle & Disposal"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"id": "scoped-loggers",
|
|
32
|
-
"code": "import { defaultLogger } from '@vielzeug/rune'\n\n// child() dot-joins namespaces automatically\nconst api = defaultLogger.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:', defaultLogger.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({ expired: false }, 'token refreshed')\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({ path: '/users', method: 'GET' }, 'request')\nlog.warn('cache miss')\nlog.error({ err: new Error('timeout') }, 'request failed')\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
|
-
"LazyBinding": "export type { LazyBinding } from './lazy';",
|
|
50
|
-
"lazy": "export { lazy } from './lazy';",
|
|
51
|
-
"createLogger": "export { createLogger, defaultLogger } from './logger';",
|
|
52
|
-
"defaultLogger": "export { createLogger, defaultLogger } from './logger';",
|
|
53
|
-
"batchTransport": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
|
|
54
|
-
"jsonTransport": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
|
|
55
|
-
"pipe": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
|
|
56
|
-
"redactTransport": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
|
|
57
|
-
"remoteTransport": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
|
|
58
|
-
"sampleTransport": "export { batchTransport, jsonTransport, pipe, 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 PipeOptions,\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 PipeOptions,\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 PipeOptions,\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 PipeOptions,\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 PipeOptions,\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 PipeOptions,\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 PipeOptions,\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 PipeOptions,\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 PipeOptions,\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 PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
69
|
-
"PipeOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
70
|
-
"RedactTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
71
|
-
"RemoteLogData": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
72
|
-
"RemoteTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
73
|
-
"RuneOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
74
|
-
"SampleTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
75
|
-
"Transport": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
76
|
-
"isLevelEnabled": "export { isLevelEnabled, PRIORITY } from './types';",
|
|
77
|
-
"PRIORITY": "export { isLevelEnabled, PRIORITY } from './types';"
|
|
78
|
-
}
|
|
79
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"apiSource": "export { buildDocument } from './_document.js';\nexport { buildCsp } from './_policy.js';\nexport { createSandbox } from './_runtime.js';\nexport { SandboxConfigurationError, SandboxError, SandboxTimeoutError } from './errors.js';\nexport type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n SandboxStateUpdateDetail,\n Unsubscribe,\n} from './types.js';\n",
|
|
3
|
-
"docs": {
|
|
4
|
-
"index": "---\ntitle: Sandbox — Sandboxed iframe runtime\ndescription: Isolated iframe runtime with a typed postMessage bridge 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 buildCsp,\n buildDocument,\n SandboxConfigurationError,\n SandboxError,\n SandboxTimeoutError,\n SandboxHandle,\n SandboxOptions,\n SandboxBridge,\n SandboxMessage,\n SandboxStateUpdateDetail,\n Unsubscribe,\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 postMessage bridge. 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| Typed postMessage protocol | <ore-icon name=\"x\" size=\"16\"></ore-icon> | `setState()` / `SandboxMessage` union |\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 guaranteed isolation, CSP enforcement, and a typed event bridge.\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(container);\n\ntry {\n // render() resolves when the document is ready\n await sandbox.render('<ore-button variant=\"primary\">Click me</ore-button>');\n\n // Push state into the sandbox\n sandbox.setState('theme', 'dark');\n} catch (error) {\n console.error('Sandbox render failed', error);\n}\n\n// Receive events from sandbox code (ready is not forwarded — internal use only)\nsandbox.onMessage((msg) => {\n if (msg.type === 'custom') console.log(msg.event, msg.detail);\n if (msg.type === 'error') console.error(msg.message);\n if (msg.type === 'resize') console.log('height:', msg.height);\n});\n\n// Re-render: await the returned Promise\nawait sandbox.render(newHtml);\n\n// Clean up — removes iframe, clears listeners\nsandbox.dispose();\n// or: using sandbox = createSandbox(container);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createSandbox()` — Creates an isolated `<iframe sandbox=\"allow-scripts\">` in the given container\n- `SandboxHandle.ready` — Promise resolving on first render's ready signal (also resolves on dispose; check `sandbox.disposed` to distinguish)\n- `SandboxHandle.disposalSignal` — `AbortSignal` aborted when the sandbox is disposed; tie async work to sandbox lifetime\n- `SandboxHandle.disposed` — Observable disposed state; check before deferred calls\n- `render(html, { signal? })` — Lazy iframe creation; returns `Promise<void>` resolving when ready, or rejecting with `SandboxTimeoutError` if the bridge never signals ready; pass `AbortSignal` to skip cancelled renders\n- `replaceBody(html)` — Replace body descendants without navigating; head scripts/styles survive while descendant state is replaced; suited to host-owned streaming markup\n- `updateStyle(id, css)` — Hot-patch a named `<style id=\"…\">` block live without re-rendering; also updates baseline for next render\n- `setState(key, value)` — Push state into the sandbox; received as `sandbox:state-update` CustomEvent\n- `setStateAll(record)` — Push multiple state values in a single postMessage; more efficient than repeated `setState()` calls for initial setup\n- `namedStyles` option — Named `<style id=\"key\">` blocks in document `<head>`; individually patchable via `updateStyle()`\n- `lang` / `title` options — Set basic language tag and `<title>` on generated documents for screen-reader correctness\n- `SandboxBridge` type — Ambient type for `window.__sandbox__` in sandbox-side TypeScript; `onState(key, handler)` subscribes to state pushed via `setState()`/`setStateAll()`\n- `custom` messages — Sandbox code emits `window.__sandbox__.emit(event, detail)` to the host\n- `resize` messages — Auto-emitted by the bridge's built-in `ResizeObserver`; no manual wiring needed\n- Strict CSP — `default-src 'none'`, inline scripts only, no network by default\n- `nonce` option — Cryptographic nonce for bridge `<script>` tag and `script-src` CSP\n- `scripts` option — Inject CDN scripts with `crossorigin=\"anonymous\"`; origins auto-added to `script-src`\n- `buildCsp()` — Build a standalone CSP string using the same `SandboxOptions`\n- `buildDocument()` — Build static isolated sandbox markup for server-side or offline use; use `createSandbox()` for host-managed runtime controls\n- Error forwarding — `onerror` + `unhandledrejection` forwarded as `{ type: 'error' }` messages\n- Disposable — `dispose()` + `[Symbol.dispose]` for `using` declarations\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: Full API reference for @vielzeug/sandbox — createSandbox, buildCsp, buildDocument, SandboxHandle, and all types.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ------ | ------- | -------------- | ------------- |\n| `createSandbox()` | Create an isolated sandboxed iframe runtime | Sync (returns handle); `render()` is async | Iframe DOM is created lazily — nothing exists until the first `render()` call |\n| `buildCsp()` | Build a CSP string from `SandboxOptions` | Sync | Invalid configuration throws `SandboxConfigurationError` |\n| `buildDocument()` | Build a complete standalone sandbox HTML document | Sync | Returns markup, not a host runtime handle; use `createSandbox()` for host-managed state or lifecycle |\n| `SandboxHandle` | Object returned by `createSandbox()` | — | `setState()`/`setStateAll()` warn in dev if called before `render()` resolves |\n| `SandboxOptions` | Unified options for `createSandbox`, `buildCsp`, `buildDocument` | — | All fields are optional; defaults documented per field below |\n| `SandboxBridge` | Bridge API at `window.__sandbox__` inside sandbox documents | — | `emit()` sends events to the host; `onState()` only receives — there is no way to call host functions directly |\n| `SandboxMessage` | Application messages the sandbox sends to the host | — | `'ready'` is not part of this union — it resolves `render()` internally instead |\n| `SandboxError` | Base error class for `@vielzeug/sandbox` | — | Use `instanceof SandboxError` to narrow package errors |\n| `SandboxConfigurationError` | Thrown for invalid origins, URLs, nonces, language tags, or style IDs | — | Fix configuration rather than relying on sanitization |\n| `SandboxTimeoutError` | Thrown by `render()` when no `'ready'` signal arrives in time | — | Extends `SandboxError`; the document is likely missing the bridge script |\n| `SandboxStateUpdateDetail` | Detail payload of the sandbox-side `sandbox:state-update` CustomEvent | — | Only relevant inside sandbox documents, not on the host |\n| `Unsubscribe` | Return type of `onMessage()` and `SandboxBridge.onState()` | — | Calling it more than once is a safe no-op |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ------ | ------- |\n| `@vielzeug/sandbox` | Main exports and types |\n| `@vielzeug/sandbox/testing` | `createSandboxTestHelpers` — postMessage simulation helpers for tests |\n\n```ts\nimport {\n buildCsp,\n buildDocument,\n createSandbox,\n SandboxConfigurationError,\n SandboxError,\n SandboxTimeoutError,\n} from '@vielzeug/sandbox';\nimport type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n SandboxStateUpdateDetail,\n Unsubscribe,\n} from '@vielzeug/sandbox';\n\nimport { createSandboxTestHelpers } from '@vielzeug/sandbox/testing';\n```\n\n## `createSandbox(container, options?)`\n\nCreates a sandboxed `<iframe>` inside `container` and returns a `SandboxHandle`.\n\n```ts\nfunction createSandbox(container: HTMLElement, options?: SandboxOptions): SandboxHandle\n```\n\nThe iframe is created lazily on the first `render()` call — `createSandbox()` is a cheap factory with no DOM work until content is ready. The iframe uses `sandbox=\"allow-scripts\"` and `referrerpolicy=\"no-referrer\"`. Content is loaded via `srcdoc` with an auto-generated CSP meta tag. The sandbox cannot access host cookies, storage, or the DOM.\n\n**Parameters**\n\n- `container` — The DOM element to append the iframe to.\n- `options` — Optional `SandboxOptions`.\n\n**Returns** a `SandboxHandle`.\n\n**Example**\n\n```ts\nconst sandbox = createSandbox(document.getElementById('preview')!);\nawait sandbox.render('<p>Hello from the sandbox</p>');\n```\n\n## `SandboxHandle`\n\n```ts\ninterface SandboxHandle {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly ready: Promise<void>;\n dispose(): void;\n onMessage(handler: (msg: SandboxMessage) => void): Unsubscribe;\n replaceBody(html: string): void;\n render(html: string, options?: { signal?: AbortSignal }): Promise<void>;\n setState(key: string, value: unknown): void;\n setStateAll(record: Record<string, unknown>): void;\n updateStyle(id: string, css: string): void;\n [Symbol.dispose](): void;\n}\n```\n\n| Member | Description |\n| ------ | ----------- |\n| `disposalSignal` | `AbortSignal` that is aborted when `dispose()` is called. Pass to `fetch` and other async operations to tie their lifetime to the sandbox. |\n| `disposed` | `true` once `dispose()` has been called. |\n| `ready` | Promise that resolves when the **first** sandbox document signals it has loaded. Also resolves if the sandbox is disposed before the first render — check `sandbox.disposed` after awaiting to distinguish the two cases. Does **not** reset on re-renders — use the Promise returned by `render()` for subsequent renders. |\n| `replaceBody(html)` | Replace `document.body.innerHTML` without navigating. Head scripts, document/window listeners, and `namedStyles` survive; body descendants, their listeners, references, form state, and scripts in replacement HTML do not. Call after `render()` resolves. |\n| `render(html, options?)` | Replace the entire sandboxed document (full page reset). Creates the iframe lazily. Returns a `Promise<void>` that resolves when the new document signals ready, or **rejects with `SandboxTimeoutError`** if no `'ready'` signal arrives within 5s. If a second `render()` starts before the first resolves, the first Promise resolves (not rejects) immediately — the document simply navigated away. Pass `options.signal` to skip if already aborted. Emits a dev warning when `html` is empty or whitespace-only. |\n| `updateStyle(id, css)` | Hot-patch a named `<style id=\"…\">` block in the live iframe via postMessage, and update the baseline for the next `render()`. No-ops if the sandbox is disposed. Safe to call before the first render (baseline only). Warns in dev if `id` is not a known key in `namedStyles`. |\n| `setState(key, value)` | Push a state value into the sandbox. Dispatches a `sandbox:state-update` CustomEvent inside the iframe. Warns in dev if called before `render()` resolves. |\n| `setStateAll(record)` | Push multiple state values in a single postMessage. Dispatches one `sandbox:state-update` CustomEvent per key inside the iframe. More efficient than calling `setState()` repeatedly for initial state setup. Warns in dev if called before `render()` resolves. |\n| `onMessage(handler)` | Subscribe to `SandboxMessage` events (`error`, `custom`, and `resize`). The `ready` lifecycle signal is not forwarded. Returns an `Unsubscribe` function. |\n| `dispose()` | Remove the iframe from the DOM and clear all listeners. Resolves any pending `ready` Promise and aborts `disposalSignal`. |\n| `[Symbol.dispose]()` | Alias for `dispose()` — enables `using sandbox = createSandbox(…)`. |\n\n::: warning Dev warnings\nCalling `render()`, `setState()`, `setStateAll()`, `updateStyle()`, or `onMessage()` on a disposed sandbox emits a warning in development (when `import.meta.env.PROD` is not `true`).\n\nCalling `setState()` or `setStateAll()` before `render()` resolves emits a dev warning — the bridge may not have set up its listener yet and the state update may be silently dropped. Always await the Promise returned by `render()` before calling either.\n\nIn production all guard paths are silent no-ops (no warnings).\n:::\n\n::: warning render() can reject\nUnlike the other guard paths above, the `SandboxTimeoutError` rejection from `render()` is **not** a dev-only warning — it fires in every build. Always attach a `.catch()` or wrap `await sandbox.render(...)` in `try`/`catch`:\n\n```ts\ntry {\n await sandbox.render(html);\n} catch (err) {\n if (err instanceof SandboxError) {\n console.error('Sandbox failed to load:', err.message);\n }\n}\n```\n:::\n\n## `SandboxOptions`\n\nUnified options for `createSandbox`, `buildCsp`, and `buildDocument`. All fields are optional.\n\n```ts\ninterface SandboxOptions {\n allowedFontOrigins?: string[];\n allowedImageOrigins?: string[];\n allowedScriptOrigins?: string[];\n allowedStyleOrigins?: string[];\n lang?: string;\n namedStyles?: Record<string, string>;\n nonce?: string;\n scripts?: string[];\n title?: string;\n}\n```\n\n| Option | Type | Default | Description |\n| ------ | ---- | ------- | ----------- |\n| `allowedFontOrigins` | `string[]` | `[]` | Absolute `http:` or `https:` origins added to `font-src`; paths, query strings, fragments, and credentials are rejected. Default directive value: `'none'`. |\n| `allowedImageOrigins` | `string[]` | `[]` | Absolute `http:` or `https:` origins added to `img-src`. `data:` is always included. |\n| `allowedScriptOrigins` | `string[]` | `[]` | Absolute `http:` or `https:` origins added to `script-src`. Merged with origins extracted from `scripts`. |\n| `allowedStyleOrigins` | `string[]` | `[]` | Absolute `http:` or `https:` origins added to `style-src`. `'unsafe-inline'` is always included. |\n| `lang` | `string` | `'en'` | Basic language tag: 2–3 letter primary language followed by optional 2–8 character subtags, such as `en`, `de`, or `zh-Hant`. |\n| `namedStyles` | `Record<string, string>` | `{}` | Named `<style id=\"key\">` blocks in document `<head>`. Keys start with a letter and contain only letters, digits, `_`, or `-`; each block is patchable via `updateStyle(id, css)`. |\n| `nonce` | `string` | `undefined` | Non-empty base64/base64url-style token added to both bridge scripts and `script-src`. In CSP Level 3 browsers the nonce suppresses `'unsafe-inline'`; `'unsafe-inline'` remains for CSP Level 2 fallback. |\n| `scripts` | `string[]` | `[]` | Absolute `http:` or `https:` script URLs injected before user content with `crossorigin=\"anonymous\"`. Their origins are added to `script-src`. |\n| `title` | `string` | `''` | Title for generated document, placed in `<title>`. Providing a title improves screen reader compatibility. |\n\n::: warning Security\n`title` and CSS content are escaped before interpolation. Origins, script URLs, `nonce`, `lang`, and `namedStyles` IDs are validated before document generation; invalid configuration throws `SandboxConfigurationError` instead of being rewritten.\n:::\n\n## `buildCsp(options?)`\n\nBuilds a strict Content-Security-Policy string for sandboxed iframe documents.\n\n```ts\nfunction buildCsp(options?: SandboxOptions): string\n```\n\nAccepts `SandboxOptions` directly. Origins from `scripts` URLs are extracted and merged with `allowedScriptOrigins` automatically. Returns a semicolon-separated CSP string with eight directives. `base-uri 'none'` is always included to block `<base>`-tag injection, and `connect-src 'none'` / `form-action 'none'` block network requests and form submission by default.\n\n**Default output (no options)**\n\n```\ndefault-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'none'; form-action 'none'; base-uri 'none'\n```\n\n**Example**\n\n```ts\nconst csp = buildCsp({\n allowedStyleOrigins: ['https://fonts.googleapis.com'],\n allowedFontOrigins: ['https://fonts.gstatic.com'],\n scripts: ['https://cdn.example.com/refine.iife.js'],\n});\n// script-src includes 'unsafe-inline' + https://cdn.example.com automatically\n```\n\n## `buildDocument(html, options?)`\n\nBuilds a complete, standalone sandbox HTML document.\n\n```ts\nfunction buildDocument(html: string, options?: SandboxOptions): string\n```\n\nIncludes the `<html lang=\"…\">` attribute, `<title>`, CSP meta tag, injected scripts, `namedStyles` rendered as `<style id=\"key\">` blocks, user content, and bridge script. Returns isolated markup for `iframe.srcdoc` or server generation (for example, through `@vielzeug/codex`).\n\n`buildDocument()` does not return a `SandboxHandle`. Use `createSandbox()` when the host must push state, replace body content, update styles, await readiness, or manage disposal.\n\nExternal scripts are placed **before** user content with `crossorigin=\"anonymous\"`, so the bridge's error handler receives full error details for cross-origin script errors. The bridge emits `ready` after preceding parser-blocking scripts execute, then observes `document.body` for resize messages.\n\n`lang` defaults to `'en'` and `title` defaults to `''` — both are HTML-escaped before interpolation.\n\n**Example**\n\n```ts\nimport { buildDocument } from '@vielzeug/sandbox';\n\nconst html = buildDocument('<p>Hello</p>', {\n lang: 'de',\n title: 'Component Preview',\n namedStyles: {\n base: 'body { font-family: sans-serif; }',\n theme: ':root { --bg: #fff; }',\n },\n});\n\niframe.srcdoc = html;\n```\n\n## Bridge Protocol\n\n### `SandboxMessage`\n\nApplication-level messages the sandbox sends to the host, received via `sandbox.onMessage(handler)`. The `ready` lifecycle signal is **intentionally excluded** — it resolves `sandbox.ready` and the Promise returned by `render()` internally and is not forwarded to subscribers.\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\n| Type | Fields | Description |\n| ---- | ------ | ----------- |\n| `error` | `message: string`, `stack?: string` | Fired on uncaught errors or unhandled promise rejections inside the sandbox. |\n| `custom` | `event: string`, `detail: unknown` | User-defined events emitted from sandbox code via `window.__sandbox__.emit(event, detail)`. |\n| `resize` | `height: number` | Emitted automatically when sandbox content height changes. The bridge script sets up a `ResizeObserver` on `document.body` — no manual wiring needed. |\n\n### `SandboxStateUpdateDetail`\n\nDetail payload of the `sandbox:state-update` CustomEvent dispatched **inside** sandbox documents by `setState()`/`setStateAll()`. Only relevant to sandbox-side code — the host never sees this type directly.\n\n```ts\ninterface SandboxStateUpdateDetail {\n key: string;\n value: unknown;\n}\n```\n\n**Emitting custom events from inside the sandbox:**\n\n```js\nwindow.__sandbox__.emit('button:click', { label: 'Save', timestamp: Date.now() });\n```\n\n**Receiving on the host:**\n\n```ts\nsandbox.onMessage((msg) => {\n if (msg.type === 'custom' && msg.event === 'button:click') {\n console.log('Button clicked:', msg.detail);\n }\n if (msg.type === 'error') {\n console.error('[sandbox]', msg.message, msg.stack);\n }\n if (msg.type === 'resize') {\n container.style.height = `${msg.height}px`;\n }\n});\n```\n\n### `SandboxBridge`\n\nThe bridge API available as `window.__sandbox__` inside sandbox documents. Export this type to add TypeScript support for sandbox-side code:\n\n```ts\ninterface SandboxBridge {\n emit(event: string, detail?: unknown): void;\n onState(key: string, handler: (value: unknown) => void): Unsubscribe;\n}\n```\n\nAdd an ambient declaration in your sandbox-side TypeScript project:\n\n```ts\n// sandbox-env.d.ts\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge;\n}\n```\n\n`onState(key, handler)` subscribes to state pushed via `sandbox.setState()`/`setStateAll()` for a specific key — it wraps the raw `sandbox:state-update` CustomEvent so sandbox-side code doesn't need to filter by key manually. Returns an `Unsubscribe` function:\n\n```ts\nconst off = window.__sandbox__.onState('theme', (value) => {\n document.body.dataset.theme = String(value);\n});\n\n// Later, stop listening:\noff();\n```\n\n### State updates\n\n`sandbox.setState(key, value)` sends a single state value into the sandbox; `sandbox.setStateAll(record)` sends multiple values in one postMessage. Both dispatch a `sandbox:state-update` CustomEvent per key, described by `SandboxStateUpdateDetail`. Inside the sandbox, either listen via the DOM directly or use `window.__sandbox__.onState()`:\n\n```js\ndocument.addEventListener('sandbox:state-update', (e) => {\n const { key, value } = e.detail;\n if (key === 'theme') document.body.dataset.theme = value;\n});\n```\n\n```ts\n// Single value\nsandbox.setState('theme', 'dark');\n\n// Multiple values in one postMessage — fires 'sandbox:state-update' twice, once per key\nsandbox.setStateAll({ theme: 'dark', locale: 'en' });\n```\n\n::: warning Security\nTreat all `SandboxMessage` data as untrusted. The sandbox controls what `custom` event payloads contain — do not execute or evaluate any message field.\n:::\n\n## Types\n\n### `Unsubscribe`\n\n```ts\ntype Unsubscribe = () => void;\n```\n\nReturn type of `onMessage()` and `SandboxBridge.onState()`. Calling it more than once is a safe no-op.\n\n## Errors\n\n### `SandboxError`\n\nBase class for all `@vielzeug/sandbox` errors. Extends `Error`.\n\n```ts\nclass SandboxError extends Error {}\n```\n\nUse `instanceof SandboxError` to narrow package errors in catch blocks. It also matches subclasses like `SandboxTimeoutError`:\n\n```ts\nimport { SandboxError } from '@vielzeug/sandbox';\n\ntry {\n await sandbox.render(html);\n} catch (err) {\n if (err instanceof SandboxError) {\n console.error(err.message);\n }\n}\n```\n\n### `SandboxConfigurationError`\n\nThrown when Sandbox configuration cannot produce a valid CSP or document. Origins must be absolute `http:` or `https:` origins without paths, query strings, fragments, or credentials. Scripts must be absolute `http:` or `https:` URLs. Nonces, basic language tags, and named style IDs must match their documented syntax.\n\n```ts\nimport { SandboxConfigurationError } from '@vielzeug/sandbox';\n\ntry {\n buildCsp({ allowedScriptOrigins: ['cdn.example.com/path'] });\n} catch (error) {\n if (error instanceof SandboxConfigurationError) console.error(error.message);\n}\n```\n\n### `SandboxTimeoutError`\n\nThrown as a rejection from `render()` when no `'ready'` signal arrives within 5 seconds, in every build (not a dev-only warning). Extends `SandboxError`. The sandbox document is most likely missing the bridge script — use `buildDocument()` to generate documents that include it, rather than hand-writing the `srcdoc` HTML.\n\n```ts\nimport { SandboxTimeoutError } from '@vielzeug/sandbox';\n\ntry {\n await sandbox.render(customHtmlMissingBridge);\n} catch (err) {\n if (err instanceof SandboxTimeoutError) {\n console.error('Sandbox never signaled ready:', err.message);\n }\n}\n```\n\n## Test Utilities\n\n`@vielzeug/sandbox/testing` exports helpers for code that integrates with the sandbox:\n\n```ts\nimport { createSandboxTestHelpers } from '@vielzeug/sandbox/testing';\n\nconst helpers = createSandboxTestHelpers(container);\n\nsandbox.render('<p>test</p>');\nhelpers.fireReady(); // simulate bridge ready signal\nhelpers.fireCustom('click', { x: 1 }); // simulate window.__sandbox__.emit()\nhelpers.fireResize(420); // simulate ResizeObserver callback\nhelpers.fireError('TypeError: x is not defined', 'at eval:1');\n```\n\nThese helpers encapsulate the internal postMessage protocol so test code doesn't need to know message shapes.\n",
|
|
6
|
-
"usage": "---\ntitle: Sandbox — Usage Guide\ndescription: How to render untrusted HTML, pass state, handle errors, configure CSP, and integrate the sandbox with your application.\n---\n\n[[toc]]\n\n::: tip New to Sandbox?\nStart with the [Overview](./index.md) for installation and a quick example, then come back here for in-depth usage patterns.\n:::\n\n## Basic Usage\n\nCreate a sandbox by passing a container element. The returned `SandboxHandle` is your entire interface to the iframe.\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\n\nconst container = document.getElementById('preview')!;\nconst sandbox = createSandbox(container);\n\nawait sandbox.render('<p>Hello from the sandbox</p>');\n```\n\n`render()` returns a `Promise<void>` that resolves when the sandbox document signals it is ready. No DOM is created until `render()` is called — `createSandbox()` is a cheap factory.\n\nFor reactive frameworks, subscribe via `onMessage` to receive `error`, `custom`, and `resize` events.\n\n## Rendering HTML\n\n`render(html)` replaces the entire sandboxed document with a new one containing your HTML in the body.\n\n```ts\nawait sandbox.render(`\n <style>body { font-family: sans-serif; }</style>\n <h1>Component Preview</h1>\n <ore-button variant=\"primary\">Click me</ore-button>\n`);\n```\n\nEach call to `render()` is a full page reset — scripts reinitialise, CSS is re-applied, and any DOM state is lost. For incremental updates, push state via `setState()` or patch styles via `updateStyle()` rather than re-rendering.\n\n## Incremental Updates with replaceBody()\n\n`replaceBody(html)` replaces `document.body.innerHTML` in the live document without navigating the iframe. Head scripts, document/window listeners, named styles, and global state survive. Body descendants, their listeners, references, form state, and scripts inside replacement HTML do not survive.\n\nUse it for streaming AI-generated output or live previews when the host owns accumulated markup.\n\n```ts\n// Initial render — sets up the document, scripts, and styles\nawait sandbox.render(`\n <script>\n document.addEventListener('sandbox:state-update', (e) => {\n document.body.dataset.theme = e.detail.value;\n });\n </script>\n <p>Loading…</p>\n`);\n\n// Subsequent updates replace body descendants\nsandbox.replaceBody('<p>First chunk arrived</p>');\nsandbox.replaceBody('<p>First chunk arrived</p><p>Second chunk…</p>');\nsandbox.replaceBody('<p>Complete response</p>');\n```\n\n**`replaceBody()` vs `render()`:**\n\n| | `render()` | `replaceBody()` |\n|---|---|---|\n| Full page reset | Yes | No |\n| Returns a Promise | Yes | No |\n| Head scripts re-run | Yes | No |\n| `namedStyles` preserved | Re-injected | Yes |\n| Body descendants/listeners | Recreated | Replaced |\n| When to use | Initial load, structural reset | Streaming markup, live preview |\n\n**`replaceBody()` must be called after `render()` resolves.** The bridge must be initialized before it can receive the replacement.\n\n## Passing State\n\n`setState(key, value)` pushes data into the sandbox without re-rendering.\n\nAlways call `setState()` after `render()` resolves — calling it before the bridge finishes initializing will silently drop the update in a real browser, and a dev warning will fire.\n\n```ts\n// Correct: await render() before pushing state\nawait sandbox.render('<div id=\"root\"></div>');\nsandbox.setState('theme', 'dark');\nsandbox.setState('user', { name: 'Alice' });\n```\n\nInside the sandbox document, listen for the `sandbox:state-update` custom event on `document`:\n\n```html\n<script>\ndocument.addEventListener('sandbox:state-update', (e) => {\n const { key, value } = e.detail;\n if (key === 'theme') document.body.dataset.theme = value;\n if (key === 'user') document.querySelector('#name').textContent = value.name;\n});\n</script>\n```\n\n## Batch State Updates\n\n`setStateAll(record)` pushes multiple state values in a single postMessage — one call instead of one `setState()` per key. Use it for initial state setup where several values become available at the same time.\n\n```ts\nawait sandbox.render('<div id=\"root\"></div>');\n\n// One postMessage instead of two setState() calls\nsandbox.setStateAll({\n theme: 'dark',\n user: { name: 'Alice' },\n});\n```\n\nThe sandbox side listens the same way as for `setState()` — each key in the record fires its own `sandbox:state-update` event.\n\n## Handling Errors\n\nSubscribe to `onMessage` before calling `render()` to catch runtime errors in sandbox content.\n\n```ts\nsandbox.onMessage((msg) => {\n if (msg.type === 'error') {\n console.error('[sandbox error]', msg.message);\n if (msg.stack) console.debug(msg.stack);\n }\n});\n```\n\nBoth synchronous errors (`window.onerror`) and unhandled promise rejections (`unhandledrejection`) are forwarded as `{ type: 'error' }` messages.\n\n### `render()` rejection\n\n`render()` rejects with a `SandboxTimeoutError` if the document never signals `'ready'` within 5 seconds — this happens in every build, not just dev. It usually means the document is missing the bridge script (custom `srcdoc` HTML built by hand instead of via `buildDocument()`). Always handle it:\n\n```ts\nimport { SandboxError } from '@vielzeug/sandbox';\n\ntry {\n await sandbox.render(html);\n} catch (err) {\n if (err instanceof SandboxError) {\n console.error('Sandbox failed to load:', err.message);\n }\n}\n```\n\nA second `render()` call superseding the first does **not** trigger this — the superseded Promise resolves, not rejects.\n\n## Injecting Scripts and Styles\n\nUse `SandboxOptions` to inject external scripts and styles into every rendered document.\n\n```ts\nconst sandbox = createSandbox(container, {\n scripts: [\n 'https://cdn.example.com/ore.js',\n 'https://cdn.example.com/refine.js',\n ],\n namedStyles: {\n base: `\n :root { --color-primary: #0066cc; }\n body { margin: 0; font-family: var(--font-sans); }\n `,\n },\n});\n```\n\nScript URLs are injected before user content. Their origins are automatically added to `script-src` in the CSP — you do not need to configure `buildCsp` separately.\n\n## Setting Document Language and Title\n\nUse `lang` and `title` to set the generated document's `<html lang=\"…\">` attribute and `<title>`. Both improve screen-reader behaviour for sandboxed content.\n\n```ts\nconst sandbox = createSandbox(container, {\n lang: 'de',\n title: 'Component Preview',\n});\n```\n\n`lang` defaults to `'en'`; use a 2–3 letter primary language with optional 2–8 character subtags, such as `de` or `zh-Hant`. `title` defaults to `''` and is HTML-escaped before document generation. Invalid language tags throw `SandboxConfigurationError`.\n\n## Hot-patching Named Styles\n\n`namedStyles` injects named `<style id=\"key\">` blocks into the document `<head>`. Named blocks can be updated live without a full re-render using `updateStyle(id, css)`.\n\n```ts\nconst sandbox = createSandbox(container, {\n namedStyles: {\n theme: ':root { --color-primary: #0066cc; --bg: #fff; }',\n },\n});\n\nawait sandbox.render('<ore-button variant=\"primary\">Click me</ore-button>');\n\n// Switch theme live — no re-render\nsandbox.updateStyle('theme', ':root { --color-primary: #bb33ff; --bg: #111; }');\n```\n\n`updateStyle()` sends a postMessage to the iframe, patching `<style id=\"theme\">` in place. It also updates the baseline so the next `render()` starts with the patched CSS. Safe to call before the first render (baseline only — no postMessage sent to an uninitialized iframe).\n\n## Resize Notifications\n\nThe bridge script automatically emits `resize` messages via a `ResizeObserver` on `document.body`. No manual wiring is needed in your sandbox content.\n\n```ts\nsandbox.onMessage((msg) => {\n if (msg.type === 'resize') {\n container.style.height = `${msg.height}px`;\n }\n});\n```\n\nThe `resize` message fires whenever the `document.body` height changes — on initial load, after content updates via `setState()`, and after style patches via `updateStyle()`.\n\n## Tying Async Work to Sandbox Lifetime\n\n`disposalSignal` is an `AbortSignal` that is aborted when the sandbox is disposed. Pass it to any async operation that should stop when the sandbox is torn down.\n\n```ts\nconst sandbox = createSandbox(container);\n\n// Polling loop tied to sandbox lifetime\nasync function poll() {\n while (!sandbox.disposalSignal.aborted) {\n const data = await fetch('/api/data', { signal: sandbox.disposalSignal }).then(r => r.json()).catch(() => null);\n if (data) sandbox.setState('data', data);\n await new Promise(resolve => setTimeout(resolve, 5000));\n }\n}\n\npoll();\n```\n\nWhen `sandbox.dispose()` is called, `disposalSignal` aborts, cancelling in-flight fetches and stopping the loop.\n\n## Configuring CSP\n\nUse `allowedStyleOrigins`, `allowedFontOrigins`, and `allowedImageOrigins` to allow CDN resources.\n\n```ts\nconst sandbox = createSandbox(container, {\n allowedStyleOrigins: ['https://fonts.googleapis.com'],\n allowedFontOrigins: ['https://fonts.gstatic.com'],\n allowedImageOrigins: ['https://images.example.com'],\n});\n```\n\nThen render HTML that uses those resources:\n\n```ts\nawait sandbox.render(`\n <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Inter\">\n <p style=\"font-family: Inter, sans-serif\">Hello</p>\n`);\n```\n\nOrigins must be absolute `http:` or `https:` origins without paths, query strings, fragments, or credentials. Script URLs must be absolute `http:` or `https:` URLs. Nonces must be non-empty base64/base64url-style tokens. Invalid configuration throws `SandboxConfigurationError`; generated CSP always includes `base-uri 'none'` to block `<base>`-tag injection.\n\n## Disposal\n\nDispose the sandbox when it is no longer needed. This removes the iframe from the DOM and clears all message listeners.\n\n```ts\n// Explicit\nsandbox.dispose();\n\n// Using explicit resource management (TypeScript 5.2+)\n{\n using sandbox = createSandbox(container);\n await sandbox.render('<p>Temporary preview</p>');\n} // sandbox.dispose() called automatically\n```\n\n## Multiple Listeners\n\n`onMessage` supports multiple independent subscriptions. Each call returns its own unsubscribe function.\n\n```ts\nconst unsubErrors = sandbox.onMessage((msg) => {\n if (msg.type === 'error') logError(msg);\n});\n\nconst unsubEvents = sandbox.onMessage((msg) => {\n if (msg.type === 'custom') handleCustomEvent(msg);\n});\n\n// Remove a single subscription\nunsubErrors();\n\n// Remove all — dispose() clears all listeners at once\nsandbox.dispose();\n```\n\n## Receiving Events from the Sandbox\n\nSandbox code calls `window.__sandbox__.emit(event, detail)` to send events to the host. Receive them via `onMessage` with `msg.type === 'custom'`.\n\n```html\n<!-- Inside sandbox content -->\n<button onclick=\"window.__sandbox__.emit('button:click', { label: 'Save' })\">Save</button>\n```\n\n```ts\n// Host\nsandbox.onMessage((msg) => {\n if (msg.type === 'custom' && msg.event === 'button:click') {\n console.log('Sandbox button clicked:', msg.detail);\n }\n});\n```\n\n**TypeScript support for sandbox-side code** — add an ambient declaration referencing `SandboxBridge`:\n\n```ts\n// sandbox-env.d.ts\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge;\n}\n```\n\n## Awaiting Subsequent Renders\n\n`render()` returns a `Promise<void>` that resolves when the new document signals ready. Await it directly for each render:\n\n```ts\nawait sandbox.render(firstHtml); // first render complete\nawait sandbox.render(secondHtml); // second render complete\n```\n\nIf a second `render()` starts before the first resolves, the first Promise resolves immediately (superseded). Multiple concurrent callers can each await their own returned Promise.\n\n## Cancelling Renders with AbortSignal\n\nPass an `AbortSignal` to `render()` to skip the render if it has already been cancelled. Useful in streaming or queued workflows:\n\n```ts\nlet controller = new AbortController();\n\nasync function streamRender(html: string) {\n controller.abort(); // cancel previous pending render\n controller = new AbortController();\n await sandbox.render(html, { signal: controller.signal });\n}\n```\n\nIf the signal is already aborted when `render()` is called, the render is skipped with no warning and no DOM change.\n\n## Building Sandbox Documents Directly\n\nUse `buildDocument` when you need static isolated markup outside `createSandbox`, such as server-generated HTML or a Codex template. Use `createSandbox` instead when the host needs state updates, body replacement, style updates, readiness, or disposal.\n\n```ts\nimport { buildDocument } from '@vielzeug/sandbox';\n\nconst html = buildDocument('<p>Hello</p>', {\n allowedStyleOrigins: ['https://fonts.googleapis.com'],\n allowedFontOrigins: ['https://fonts.gstatic.com'],\n namedStyles: {\n theme: ':root { --bg: #fff; }',\n },\n});\n\n// html is a complete <!doctype html> document — assign directly to srcdoc\niframe.srcdoc = html;\n```\n\nUse `buildCsp` if you only need the CSP string for an existing document template:\n\n```ts\nimport { buildCsp } from '@vielzeug/sandbox';\n\nconst csp = buildCsp({ allowedFontOrigins: ['https://fonts.gstatic.com'] });\n// → \"default-src 'none'; ... font-src https://fonts.gstatic.com; ...\"\n```\n\n## Testing\n\nUse `createSandboxTestHelpers` from the `/testing` subpath to simulate sandbox→host messages without a real `srcdoc` script execution (jsdom does not execute iframe `srcdoc` scripts).\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\nimport { createSandboxTestHelpers } from '@vielzeug/sandbox/testing';\nimport { describe, expect, it } from 'vitest';\n\ndescribe('preview panel', () => {\n it('forwards a custom event from the sandbox', async () => {\n const container = document.createElement('div');\n const sandbox = createSandbox(container);\n const helpers = createSandboxTestHelpers(container);\n\n const received: unknown[] = [];\n\n sandbox.onMessage((msg) => received.push(msg));\n\n const renderPromise = sandbox.render('<button>Save</button>');\n\n helpers.fireReady(); // simulate the bridge script's initial postMessage\n await renderPromise;\n\n helpers.fireCustom('button:click', { label: 'Save' });\n expect(received).toEqual([{ type: 'custom', event: 'button:click', detail: { label: 'Save' } }]);\n\n sandbox.dispose();\n });\n});\n```\n\n`SandboxTestHelpers` also exposes `fireResize(height)` and `fireError(message, stack?)` for testing resize and error handling without a live browser.\n\n## Framework Integration\n\nCreate the sandbox once per mount and dispose it on unmount — the container element is stable for the component's lifetime.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createSandbox } from '@vielzeug/sandbox';\n\nfunction SandboxPreview({ html }: { html: string }) {\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n const sandbox = createSandbox(containerRef.current);\n\n sandbox.render(html);\n\n return () => sandbox.dispose();\n }, [html]);\n\n return <div ref={containerRef} />;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { createSandbox, type SandboxHandle } from '@vielzeug/sandbox';\n\nconst props = defineProps<{ html: string }>();\nconst containerRef = ref<HTMLDivElement>();\nlet sandbox: SandboxHandle | undefined;\n\nonMounted(() => {\n if (!containerRef.value) return;\n sandbox = createSandbox(containerRef.value);\n sandbox.render(props.html);\n});\n\nonUnmounted(() => sandbox?.dispose());\n</script>\n\n<template>\n <div ref=\"containerRef\" />\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onMount } from 'svelte';\n import { createSandbox } from '@vielzeug/sandbox';\n\n export let html: string;\n let container: HTMLDivElement;\n\n onMount(() => {\n const sandbox = createSandbox(container);\n\n sandbox.render(html);\n\n return () => sandbox.dispose();\n });\n</script>\n\n<div bind:this={container}></div>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n**With Codex:**\nThe `generate-sandbox-document` and `get-state-bridge-spec` MCP tools in `@vielzeug/codex` are designed to work with Sandbox. They generate complete sandbox-ready document templates and document the bridge protocol.\n\n```ts\n// After codex generates an HTML document:\nawait sandbox.render(generatedDocument);\n```\n\n**With Refine:**\nInject the Refine/Ore runtime into the sandbox via `scripts`:\n\n```ts\nconst sandbox = createSandbox(container, {\n scripts: ['https://cdn.example.com/refine.iife.js'],\n namedStyles: {\n theme: '/* refine theme tokens */',\n },\n});\n\nawait sandbox.render('<ore-card><ore-button>Save</ore-button></ore-card>');\n```\n\n## Best Practices\n\n- **Await `render()` before calling `setState()`/`setStateAll()`** — both warn in dev if called before the bridge is ready. Use `setStateAll()` to bootstrap several values in one postMessage instead of calling `setState()` repeatedly.\n- **Use `await sandbox.render(html)` for each render** — `render()` returns a `Promise<void>` that resolves when the document is ready. No separate readiness API is needed.\n- **Use `updateStyle()` for theme switching** — updating a named style avoids a full `render()` and preserves current document state.\n- **Check `disposed` before deferred calls** — across async operations, check `sandbox.disposed` before calling any method to avoid spurious dev warnings.\n- **Tie async work to `disposalSignal`** — pass `disposalSignal` to `fetch` and other async operations so they cancel automatically on dispose.\n- **Treat all messages as untrusted** — sandbox code controls `SandboxMessage` payloads. Do not `eval()` or execute any message field.\n- **One sandbox per preview** — `createSandbox()` is a cheap factory; create a new sandbox per user session or component rather than reusing across unrelated renders.\n- **Use `using` in functions** — in TypeScript 5.2+ contexts, `using` guarantees cleanup even on exceptions.\n- **Use `replaceBody()` or `setState()` for incremental updates** — `render()` resets document state. `replaceBody()` replaces body descendants; `setState()` updates live code without replacing DOM.\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": "build-csp",
|
|
12
|
-
"code": "import { buildCsp } from '@vielzeug/sandbox'\n\n// Default strict CSP — no external resources allowed\nconst defaultCsp = buildCsp()\nconsole.log('Default CSP:')\nconsole.log(defaultCsp)\n\n// Allow Google Fonts (stylesheet + font files)\nconst fontsCsp = buildCsp({\n allowedStyleOrigins: ['https://fonts.googleapis.com'],\n allowedFontOrigins: ['https://fonts.gstatic.com'],\n})\nconsole.log('\\nWith Google Fonts:')\nconsole.log(fontsCsp)\n\n// Allow a CDN script origin and an image host\nconst cdnCsp = buildCsp({\n allowedScriptOrigins: ['https://cdn.example.com'],\n allowedImageOrigins: ['https://images.example.com'],\n})\nconsole.log('\\nWith CDN + images:')\nconsole.log(cdnCsp)",
|
|
13
|
-
"name": "Build CSP String"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"id": "build-document",
|
|
17
|
-
"code": "import { buildDocument } from '@vielzeug/sandbox'\n\n// Build a complete standalone sandbox HTML document — useful for SSR previews,\n// static artifacts, or anywhere you need the document string without a live iframe.\nconst html = buildDocument('<h1>Hello from the sandbox</h1><p>No live iframe required.</p>', {\n lang: 'en',\n title: 'Sandbox Preview',\n namedStyles: {\n base: 'body { font-family: system-ui, sans-serif; margin: 0; padding: 1rem; }',\n },\n})\n\nconsole.log('Document length:', html.length, 'characters')\nconsole.log('Has <html lang=\"en\">:', html.includes('lang=\"en\"'))\nconsole.log('Has <title>Sandbox Preview</title>:', html.includes('<title>Sandbox Preview</title>'))\nconsole.log('Has named style block #base:', html.includes('<style id=\"base\">'))\nconsole.log('Includes the bridge script:', html.includes('window.__sandbox__'))\n\nconsole.log('\\nFirst 300 characters:')\nconsole.log(html.slice(0, 300))",
|
|
18
|
-
"name": "Build Document"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"id": "error-normalize",
|
|
22
|
-
"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))",
|
|
23
|
-
"name": "Normalize Errors with SandboxError"
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
|
-
"typeSignatures": {
|
|
27
|
-
"buildDocument": "export { buildDocument } from './_document.js';",
|
|
28
|
-
"buildCsp": "export { buildCsp } from './_policy.js';",
|
|
29
|
-
"createSandbox": "export { createSandbox } from './_runtime.js';",
|
|
30
|
-
"SandboxConfigurationError": "export { SandboxConfigurationError, SandboxError, SandboxTimeoutError } from './errors.js';",
|
|
31
|
-
"SandboxError": "export { SandboxConfigurationError, SandboxError, SandboxTimeoutError } from './errors.js';",
|
|
32
|
-
"SandboxTimeoutError": "export { SandboxConfigurationError, SandboxError, SandboxTimeoutError } from './errors.js';",
|
|
33
|
-
"SandboxBridge": "export type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n SandboxStateUpdateDetail,\n Unsubscribe,\n} from './types.js';",
|
|
34
|
-
"SandboxHandle": "export type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n SandboxStateUpdateDetail,\n Unsubscribe,\n} from './types.js';",
|
|
35
|
-
"SandboxMessage": "export type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n SandboxStateUpdateDetail,\n Unsubscribe,\n} from './types.js';",
|
|
36
|
-
"SandboxOptions": "export type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n SandboxStateUpdateDetail,\n Unsubscribe,\n} from './types.js';",
|
|
37
|
-
"SandboxStateUpdateDetail": "export type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n SandboxStateUpdateDetail,\n Unsubscribe,\n} from './types.js';",
|
|
38
|
-
"Unsubscribe": "export type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n SandboxStateUpdateDetail,\n Unsubscribe,\n} from './types.js';"
|
|
39
|
-
}
|
|
40
|
-
}
|