···7979 - ✓ Form serialization and submission
8080 - ✓ Request/response headers customization
81818282-## To-Do
8383-8482### Markup Based Reactivity
85838684**Goal:** Allow Volt apps to declare state, bindings, and behavior entirely in HTML markup
···9290 - ✓ Control-flow directives (`data-volt-for`, `data-volt-if`, `data-volt-else`) with lifecycle-safe teardown.
9391 - ✓ Declarative event system (`data-volt-on:*`) with helper surface for list mutations and plugin hooks.
9492 - ✓ SSR compatibility helpers
9595- - Sandboxed expression evaluator
9393+ - ✓ Sandboxed expression evaluator
9494+9595+## To-Do
96969797### Streaming & Patch Engine
9898···176176**Outcome:** Volt.js enables declarative animations and view transitions alongside reactivity.
177177**Deliverables:**
178178 - `data-volt-transition` directive with enter/leave transitions
179179- - Transition modifiers (duration, delay, opacity, scale, etc.)
180180- - View Transitions API integration (when available)
181181- - CSS-based transition helpers
179179+ - Transition modifiers (duration, delay, opacity, scale, etc.)
180180+ - View Transitions API integration (when available)
181181+ - CSS-based transition helpers
182182 - `data-volt-animate` plugin for keyframe animations
183183- - Timing utilities and easing functions
183183+ - Timing utilities and easing functions
184184 - Integration with `data-volt-if` and `data-volt-show` for automatic transitions
185185186186### Background Requests & Reactive Polling
···188188**Goal:** Enable declarative background data fetching and periodic updates within the Volt.js runtime.
189189**Outcome:** Volt.js elements can fetch or refresh data automatically based on time, visibility, or reactive conditions.
190190**Deliverables:**
191191- - `data-volt-fetch` attribute for declarative background requests
192192- - Configurable polling intervals, delays, and signal-based triggers
193191 - `data-volt-visible` for fetching when an element enters the viewport (`IntersectionObserver`)
192192+ - `data-volt-fetch` attribute for declarative background requests
193193+ - Configurable polling intervals, delays, and signal-based triggers
194194+ - Automatic cancellation of requests when elements are unmounted
195195+ - Conditional execution tied to reactive signals
196196+ - Integration hooks for loading and pending states
194197 - Background task scheduler with priority management
195195- - Automatic cancellation of requests when elements are unmounted
196196- - Conditional execution tied to reactive signals
197197- - Integration hooks for loading and pending states
198198199199### Navigation & History Management
200200···202202**Outcome:** Volt.js provides enhanced navigation behavior with minimal overhead and full accessibility support.
203203**Deliverables:**
204204 - `data-volt-navigate` for intercepting link and form actions
205205- - Integration with the History API (`pushState`, `replaceState`, `popState`)
206206- - Reactive synchronization of route and signal state
207207- - Smooth page and fragment transitions coordinated with Volt’s signal system
208208- - Native back/forward button support
209209- - Scroll position persistence and restoration
210210- - Optional preloading of linked resources on hover or idle
205205+ - Integration with the History API (`pushState`, `replaceState`, `popState`)
206206+ - Reactive synchronization of route and signal state
207207+ - Smooth page and fragment transitions coordinated with Volt’s signal system
208208+ - Native back/forward button support
209209+ - Scroll position persistence and restoration
210210+ - Preloading of linked resources on hover or idle
211211 - `data-volt-url` for declarative history updates
212212- - Optional View Transition API integration for animated route changes
212212+ - View Transition API integration for animated route changes
213213214214### Inspector & Developer Tools
215215
+1-1
cli/README.md
···11-# Volt CLI
11+# Volt Developer CLI
2233Development tools for the Volt.js project. Uses:
44
···11+# Expression Evaluation
22+33+Volt.js evaluates JavaScript-like expressions in HTML templates using a sandboxed recursive descent parser.
44+The evaluator is CSP-compliant and does not use `eval()` or `new Function()`.
55+66+## Supported Syntax
77+88+The expression language supports a subset of JavaScript:
99+1010+- Standard literals (numbers, strings, booleans, null, undefined)
1111+- Arithmetic operators (`+`, `-`, `*`, `/`, `%`)
1212+- Comparison operators (`===`, `!==`, `<`, `>`, `<=`, `>=`)
1313+- Logical operators (`&&`, `||`, `!`)
1414+- Ternary operator (`? :`).
1515+1616+Property access works via dot notation (`user.name`) or bracket notation (`items[0]`).
1717+Method calls are supported on any object, including chaining (`text.trim().toUpperCase()`).
1818+Arrow functions work with single-expression bodies for use in array methods like `filter`, `map`, and `reduce`.
1919+2020+Array and object literals can be created inline, with spread operator support (`...`) for both arrays and objects.
2121+Signals are automatically unwrapped when referenced in expressions.
2222+2323+## Security Model
2424+2525+The evaluator implements a balanced sandbox that blocks dangerous operations while attempting to preserve flexibility for most use cases.
2626+2727+### Blocked Access
2828+2929+Three property names are unconditionally blocked to prevent prototype pollution: `__proto__`, `constructor`, and `prototype`.
3030+These restrictions apply to all access patterns including dot notation, bracket notation, and object literal keys.
3131+3232+The following global names are blocked even if present in scope:
3333+`Function`, `eval`, `globalThis`, `window`, `global`, `process`, `require`, `import`, `module`, `exports`.
3434+3535+### Allowed Operations
3636+3737+Standard constructors and utilities remain accessible: `Array`, `Object`, `String`, `Number`, `Boolean`, `Date`, `Math`, `JSON`, `RegExp`, `Map`, `Set`, `Promise`.
3838+3939+All built-in methods on native types (strings, arrays, objects, etc.) are permitted. Signal methods (`get`, `set`, `subscribe`) are explicitly allowed even though `constructor` is otherwise blocked.
4040+4141+### Error Handling
4242+4343+Expressions containing unsafe operations or syntax errors are caught, logged to the console, and return `undefined` rather than throwing. This prevents malicious or malformed expressions from breaking the application.
4444+4545+## Guidelines
4646+4747+### Performance
4848+4949+Expressions are parsed on every evaluation. For optimal performance, keep expressions simple and use computed signals for complex calculations.
5050+The evaluator automatically tracks signal dependencies so only affected bindings re-evaluate when signals change.
5151+5252+### Best Practices
5353+5454+- Use computed signals for logic that appears in multiple bindings or involves expensive operations.
5555+- Never use untrusted user input directly in expressions without validation.
5656+- Prefer simple, readable expressions in templates over complex nested operations.
5757+- Structure your scope data with consistent shapes (or consistent types) to avoid runtime errors.
+19-105
docs/overview.md
···8899It combines HTML-driven behavior via `data-volt-*` attributes with signal-based reactivity.
10101111-## Architecture
1212-1313-The framework consists of three layers:
1414-1515-### Reactivity Layer
1616-1717-Signals are the foundation of Volt's reactive system:
1818-1919-```js
2020-import { signal, computed, effect } from 'volt';
2121-2222-const count = signal(0);
2323-const doubled = computed(() => count.get() * 2, [count]);
2424-2525-effect(() => console.log('Count:', count.get()), [count]);
2626-```
2727-2828-- **`signal()`** creates mutable reactive state
2929-- **`computed()`** derives values from signals
3030-- **`effect()`** runs side effects when dependencies change
3131-3232-No reactivity scheduler. Signals notify subscribers directly on change.
3333-3434-### Binding System
3535-3636-Bindings connect signals to DOM via `data-volt-*` attributes:
3737-3838-```html
3939-<div id="app">
4040- <p data-volt-text="count">0</p>
4141- <button data-volt-on-click="increment">+</button>
4242- <div data-volt-if="isPositive">Positive</div>
4343-</div>
4444-```
4545-4646-```js
4747-mount(document.querySelector('#app'), {
4848- count,
4949- isPositive,
5050- increment: () => count.set(count.get() + 1)
5151-});
5252-```
5353-5454-Core bindings:
5555-5656-- `data-volt-text` - Update text content
5757-- `data-volt-html` - Update HTML content
5858-- `data-volt-class` - Toggle CSS classes
5959-- `data-volt-on-*` - Attach event handlers
6060-- `data-volt-if` - Conditional rendering
6161-- `data-volt-for` - List rendering
6262-6363-### Plugin System
6464-6565-Extend functionality via custom `data-volt-*` bindings:
6666-6767-```js
6868-import { registerPlugin } from 'volt';
6969-7070-registerPlugin('tooltip', (context, value) => {
7171- // Custom binding logic
7272-});
7373-```
7474-7575-See [Plugin Spec](./plugin-spec.md) for details.
7676-7777-## Key Concepts
7878-7979-### Signals Update DOM Directly
8080-8181-Bindings subscribe to signals and update the real DOM when values change.
8282-8383-### HTML Drives Behavior
8484-8585-Declare UI structure and interactivity in HTML. JavaScript provides state and handlers.
8686-8787-### Explicit Dependencies
8888-8989-Computed signals and effects declare dependencies explicitly:
9090-9191-```js
9292-computed(() => a.get() + b.get(), [a, b]) // Both deps listed
9393-```
9494-9595-### Cleanup Management
9696-9797-`mount()` returns a cleanup function. All bindings register their cleanup to prevent memory leaks:
9898-9999-```js
100100-const cleanup = mount(element, scope);
101101-// Later:
102102-cleanup(); // Unsubscribes all bindings
103103-```
1111+## Features
10412105105-## Examples
1313+### Reactivity
10614107107-### Counter
1515+- Signal-based state management with `signal`, `computed`, and `effect` primitives
1616+- Predicatable direct DOM updates without virtual DOM diffing
10817109109-Simple counter demonstrating basic reactivity:
1818+### Hypermedia Integration
11019111111-- Location: `examples/counter/`
112112-- Shows: signals, computed values, conditional rendering, class bindings
2020+- Declarative HTTP requests with `data-volt-get`, `data-volt-post`, `data-volt-put`, `data-volt-patch`, and `data-volt-delete`
2121+- Multiple DOM swap strategies for server-rendered HTML fragments/partials
2222+- "Smart" retry with exponential backoff for failed requests
2323+- Automatic form serialization
11324114114-### TodoMVC
2525+### Plugins
11526116116-Complete todo app with filtering and editing:
2727+- Built-In
2828+ - State persistence across page loads using `localStorage`, `sessionStorage`, or `IndexedDB`
2929+ - Scroll management including position restore, scroll-to, scroll-spy, and smooth scrolling
3030+ - URL synchronization for query parameters and hash-based routing
3131+- Extensibility
3232+ - Custom plugin system via `registerPlugin` for domain-specific bindings
3333+ - Global lifecycle hooks for mount, unmount, and binding creation
3434+ - Automatic cleanup management
11735118118-- Location: `examples/todomvc/`
119119-- Shows: list rendering, event handling, computed filters, state mapping
120120-- Uses: Volt CSS (classless framework) for styling
121121-122122-## Design Constraints
3636+### Design Constraints
1233712438- Core runtime under 15 KB gzipped
12539- Zero dependencies
+61
examples/TODO.md
···11+# Example TODO
22+33+Planned examples to demonstrate Volt.js features & show Volt.css
44+55+All examples use declarative mode by default (data-volt-state, charge()). This ensures users can build functional apps without writing JavaScript.
66+77+## Example Set
88+99+### Counter
1010+1111+Basic reactivity demonstration showing core primitives.
1212+1313+- **Features**: data-volt-state, data-volt-computed, data-volt-text, data-volt-on-click, data-volt-class
1414+- **Shows**: Inline state declaration, computed values, event handlers that modify signals, conditional classes
1515+- **Structure**: Single index.html file with inline state
1616+1717+### Form Validation
1818+1919+Real-world form handling with reactive validation.
2020+2121+- **Features**: data-volt-state, data-volt-model, data-volt-if/else, data-volt-computed for validation, data-volt-bind:disabled
2222+- **Shows**: Two-way form binding, computed validation rules, conditional error messages, reactive button states
2323+- **Structure**: Single index.html with validation logic in computed expressions
2424+2525+### Persistent Settings
2626+2727+Settings panel that survives page refresh.
2828+2929+- **Features**: data-volt-state, data-volt-model, persist plugin with localStorage
3030+- **Shows**: Plugin usage, state persistence across page loads, settings form
3131+- **Structure**: Single index.html demonstrating data-volt-persist
3232+3333+### HTTP Todo List
3434+3535+Full-featured todo app with server persistence and hypermedia.
3636+3737+- **Features**: data-volt-get/patch/post/delete, data-volt-swap, data-volt-indicator, data-volt-retry, data-volt-for
3838+- **Shows**: Hypermedia approach, server communication, DOM swapping, loading states, error handling, smart retry, list rendering
3939+- **Structure**: index.html + minimal bootstrap script to fetch initial todos
4040+4141+Made with a Go server with filesystem-based JSON persistence
4242+4343+**Implementation**:
4444+4545+- main.go with model, view & controller files
4646+- Filesystem-based storage (todos.json)
4747+- REST endpoints:
4848+ - GET /todos - List all todos (returns HTML fragment)
4949+ - POST /todos - Create new todo (returns HTML fragment)
5050+ - PATCH /todos/:id - Update todo (partial - complete, edit text)
5151+ - DELETE /todos/:id - Delete todo (returns empty or success fragment)
5252+- Returns HTML fragments for Volt's DOM swapping
5353+- REST API for demonstration
5454+5555+**Storage Format**:
5656+5757+```json
5858+[
5959+ {"id": "1", "text": "Example todo", "completed": false}
6060+]
6161+```
+94-7
lib/src/core/evaluator.ts
···11/**
22 * Safe expression evaluation with operators support
33 *
44- * Implements a recursive descent parser for expressions without using eval()
44+ * Implements a recursive descent parser for expressions without using eval().
55+ * Includes sandboxing to prevent prototype pollution and sandbox escape attacks.
56 */
6778import type { Dep, Scope } from "$types/volt";
891010+/**
1111+ * Blocked properties to prevent prototype pollution and sandbox escape
1212+ */
1313+const DANGEROUS_PROPERTIES = new Set(["__proto__", "prototype", "constructor"]);
1414+1515+const SAFE_GLOBALS = new Set([
1616+ "Array",
1717+ "Object",
1818+ "String",
1919+ "Number",
2020+ "Boolean",
2121+ "Date",
2222+ "Math",
2323+ "JSON",
2424+ "RegExp",
2525+ "Map",
2626+ "Set",
2727+ "Promise",
2828+]);
2929+3030+const DANGEROUS_GLOBALS = new Set([
3131+ "Function",
3232+ "eval",
3333+ "globalThis",
3434+ "window",
3535+ "global",
3636+ "process",
3737+ "require",
3838+ "import",
3939+ "module",
4040+ "exports",
4141+]);
4242+4343+/**
4444+ * Validates that a property name is safe to access
4545+ */
4646+function isSafeProp(key: unknown): boolean {
4747+ if (typeof key !== "string" && typeof key !== "number") {
4848+ return true;
4949+ }
5050+5151+ const keyStr = String(key);
5252+ return !DANGEROUS_PROPERTIES.has(keyStr);
5353+}
5454+5555+/**
5656+ * Validates that accessing a property on an object is safe
5757+ */
5858+function isSafeAccess(object: unknown, key: unknown): boolean {
5959+ if (!isSafeProp(key)) {
6060+ return false;
6161+ }
6262+6363+ if (typeof object === "function") {
6464+ const keyStr = String(key);
6565+ if (keyStr === "constructor" && object.name && !SAFE_GLOBALS.has(object.name)) {
6666+ return false;
6767+ }
6868+ }
6969+7070+ return true;
7171+}
7272+973type TokenType =
1074 | "NUMBER"
1175 | "STRING"
···42106 | "OR_OR"
43107 | "EOF";
441084545-/**
4646- * Token representing a lexical unit
4747- */
48109type Token = { type: TokenType; value: unknown; start: number; end: number };
491105050-/**
5151- * Tokenize an expression string into a stream of tokens
5252- */
53111function tokenize(expr: string): Token[] {
54112 const tokens: Token[] = [];
55113 let pos = 0;
···458516 this.consume("RPAREN", "Expected ')' after arguments");
459517460518 if (typeof object === "function") {
519519+ const func = object as { name?: string };
520520+ if (func.name === "Function" || func.name === "eval") {
521521+ throw new Error("Cannot call dangerous function");
522522+ }
461523 object = (object as (...args: unknown[]) => unknown)(...args);
462524 } else {
463525 throw new TypeError("Attempting to call a non-function value");
···491553 private callMethod(object: unknown, methodName: string, args: unknown[]): unknown {
492554 if (object === null || object === undefined) {
493555 throw new Error(`Cannot call method '${methodName}' on ${object}`);
556556+ }
557557+558558+ if (!isSafeAccess(object, methodName)) {
559559+ throw new Error(`Unsafe method call: ${methodName}`);
494560 }
495561496562 const method = (object as Record<string, unknown>)[methodName];
···577643 if (this.match("DOT_DOT_DOT")) {
578644 const spreadValue = this.parseExpr();
579645 if (typeof spreadValue === "object" && spreadValue !== null && !Array.isArray(spreadValue)) {
646646+ for (const key of Object.keys(spreadValue)) {
647647+ if (!isSafeProp(key)) {
648648+ throw new Error(`Unsafe property in spread: ${key}`);
649649+ }
650650+ }
580651 Object.assign(object, spreadValue);
581652 } else {
582653 throw new Error("Spread operator can only be used with objects in object literals");
···592663 throw new Error("Expected property key in object literal");
593664 }
594665666666+ if (!isSafeProp(key)) {
667667+ throw new Error(`Unsafe property key in object literal: ${key}`);
668668+ }
669669+595670 this.consume("COLON", "Expected ':' after property key");
596671 const value = this.parseExpr();
597672 object[key] = value;
···727802 return undefined;
728803 }
729804805805+ if (!isSafeAccess(object, key)) {
806806+ throw new Error(`Unsafe property access: ${String(key)}`);
807807+ }
808808+730809 if (isSignal(object) && (key === "get" || key === "set" || key === "subscribe")) {
731810 return (object as Record<string, unknown>)[key as string];
732811 }
···745824 }
746825747826 private resolvePropPath(path: string): unknown {
827827+ if (!isSafeProp(path)) {
828828+ throw new Error(`Unsafe property access: ${path}`);
829829+ }
830830+831831+ if (DANGEROUS_GLOBALS.has(path)) {
832832+ throw new Error(`Access to dangerous global: ${path}`);
833833+ }
834834+748835 if (!(path in this.scope)) {
749836 return undefined;
750837 }
+3-1
lib/src/index.ts
···2020export { clearPlugins, getRegisteredPlugins, hasPlugin, registerPlugin, unregisterPlugin } from "$core/plugin";
2121export { computed, effect, signal } from "$core/signal";
2222export { deserializeScope, hydrate, isHydrated, isServerRendered, serializeScope } from "$core/ssr";
2323-export { persistPlugin, registerStorageAdapter, scrollPlugin, urlPlugin } from "$plugins";
2323+export { persistPlugin, registerStorageAdapter } from "$plugins/persist";
2424+export { scrollPlugin } from "$plugins/scroll";
2525+export { urlPlugin } from "$plugins/url";
2426export type {
2527 AsyncEffectFunction,
2628 AsyncEffectOptions,
+3-1
lib/src/main.ts
···11-import { persistPlugin, scrollPlugin, urlPlugin } from "$plugins";
11+import { persistPlugin } from "$plugins/persist";
22+import { scrollPlugin } from "$plugins/scroll";
33+import { urlPlugin } from "$plugins/url";
24import { computed, effect, mount, registerPlugin, signal } from "$volt";
3546registerPlugin("persist", persistPlugin);
-9
lib/src/plugins/index.ts
···11-/**
22- * Built-in plugins for Volt.js
33- *
44- * All plugins require explicit registration via registerPlugin()
55- */
66-77-export { persistPlugin, registerStorageAdapter } from "./persist";
88-export { scrollPlugin } from "./scroll";
99-export { urlPlugin } from "./url";
+2-1
lib/src/plugins/persist.ts
···106106 },
107107} satisfies StorageAdapter;
108108109109+let dbPromise: Optional<Promise<IDBDatabase>>;
110110+109111/**
110112 * Open or create the IndexedDB database
111113 */
112112-let dbPromise: Optional<Promise<IDBDatabase>>;
113114function openDB(): Promise<IDBDatabase> {
114115 if (dbPromise) return dbPromise;
115116