Core Concepts
Architecture & Philosophy
Wizz is a compiler first. Everything a framework usually decides at runtime — which nodes depend on which state, how a list reconciles, which CSS applies to which element — Wizz decides at build time, so the browser runs only the instructions your component actually needs.
Philosophy
Three convictions shape every design decision:
The platform is enough
Modern browsers ship ES modules, microtasks, BroadcastChannel, and the DOM APIs a UI needs. Wizz targets them directly instead of abstracting over them.
Constraints are features
A small, statically checkable template language lets the compiler prove what changes, and reject what it cannot prove, with a stable diagnostic code.
Server first, client second
Pages render on the server by default. The client adopts that markup rather than rebuilding it, and falls back safely when it cannot.
Tooling is part of the product
A versioned JSON diagnostics envelope and an MCP server make the compiler as legible to CI and AI agents as it is to people.
Why zero dependencies
Wizz installs without npm. The CLI tarball carries the compiler, runtime, templates, and build tooling, and the generated dist/ imports nothing but its own files. That buys you:
- No supply-chain surface in your app — there is no transitive tree to audit, pin, or patch.
- No version drift between framework packages, because there is only one artifact.
- Portable output: a static host serves
dist/as-is, and the optional node host uses only Node built-ins. - Small payloads: the browser loads the router, the route's compiled modules, and nothing else.
Why no Virtual DOM
A Virtual DOM re-runs your render function and diffs two trees to discover what changed. Wizz already knows: the analyzer records which reactive names every expression reads, so an assignment to count updates exactly the text nodes and attributes that read count — no tree, no diff, no reconciliation pass over static markup.
The compiler pipeline
Every component passes through the same ordered stages. Callers normally use compile() or compileServer(), which compose them:
Wizz component source
-> parseComponent(source) tokenize, extract <script>/<wizz:head>/<wizz:style>,
imports, export let props, persist() markers
-> analyzeDependencies(payload) map each expression to the reactive names it reads
-> assignNodeIds(payload) stable data-wizz-id for every dynamic node
-> generateComponent(payload) client module (mountComponent)
generateServerComponent(payload) server module (renderComponent)
generateHydrationFunction(...) hydratable build (hydrateComponent / hydrateRoot)
-> { source, payload, sourceMap, version }- Parse — the tokenizer and Pratt expression parser build a template AST; extractors lift the script, head, and style blocks; the prop extractor and state scanner find
export letandletdeclarations andpersist()markers. - Analyze — the dependency analyzer maps each template expression to the reactive identifiers it reads; the ID assigner gives every dynamic node a stable
data-wizz-id. - Generate — the code builder emits a client module, and on request a DOM-free server module and a hydratable client build. The assignment interceptor rewrites statement-level mutations so they notify the scheduler.
What the compiler emits
Take the smallest interesting component:
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<main>
<button on:click={increment}>Clicks: {count}</button>
</main>The client module is an ordinary ES module whose default export mounts it. Simplified, it has this shape:
// Generated by Wizz 1.17.0 (component syntax 1.4.1, generated output 1.8.2).
export default function mountComponent(target, props = {}) {
let count = 0;
function increment() {
count += 1;
schedule({ count: true }); // inserted by the assignment interceptor
}
// create(): plain DOM construction, no templates parsed at runtime
const main = document.createElement("main");
const button = document.createElement("button");
button.setAttribute("data-wizz-id", "1");
button.addEventListener("click", increment);
// ...
function update(changed) {
if (changed.count) {
textNode.data = String(count); // only the node that reads count
}
}
target.appendChild(main);
return { destroy() { /* hooks, children, listeners, root */ } };
}The real output also wires child components, keyed list updaters, head management, and scoped styles — but there is no runtime library behind it. build.js writes a source map for each module with an author script, so breakpoints land on your original .wizz lines.
The update model
- Marking: an intercepted assignment marks its variable as changed.
- Batching: marks combine until the next microtask, so a handler that changes five variables triggers one update.
- Targeted writes: the update function checks each mark and touches only the nodes that read it —
value,checked, anddisabledas DOM properties, everything else as attributes. - Lists: keyed
{#each}blocks retain, move, create, and remove nodes by key; keyless blocks reconcile by index. - Children: parents push new prop values through the child's
setProps(); a child is never remounted for a prop change.
SSR & hydration
Server rendering is an explicit compile target. For every file whose whole component tree is renderable, the build emits three modules: Name.js (client), Name.server.js (DOM-free renderer), and Name.hydrate.js (adopts server markup).
import { renderComponent, serializeInitialState } from "./dist/pages/Home.server.js";
const { html, head, state } = renderComponent({});
const stateScript = serializeInitialState(state);
// '<script type="application/wizz-state">{"count":0}</script>'
// Deliver html INSIDE the mount point and the state script as its SIBLING:
const body = '<div id="app">' + html + "</div>" + stateScript;wizz devand the node adapter match the URL against the route manifest and callrenderComponent()per request.- The router finds the
application/wizz-statescript beside#app, imports the hydratable build, and callshydrateComponent(). - The hydration walk verifies tags,
data-wizz-ids, and text against the template, then attaches listeners. Child components are adopted in place throughhydrateRoot(). - On any mismatch it logs one
[wizz] hydration mismatchwarning, clears the target, and mounts fresh — never a duplicate DOM.
Scoped CSS delivery
Each <wizz:style> block is scoped by a generated data-wizz-s attribute on the selector's terminal element, and its @keyframes names gain the same suffix. Delivery never duplicates: wizz dev server-renders one style tag per styled component, hydration adopts it, and client mounts share a refcounted stylesheet. wizz build extracts every scoped rule into dist/app.css and links it from the shell, so production pages are styled from the first paint.
Versioning contract
Wizz versions three things independently, all defined in src/compiler/version.js and stamped on the first line of every generated module:
// Generated by Wizz 1.17.0 (component syntax 1.4.1, generated output 1.8.2). Edits will be overwritten.| Version | Covers | Guarantee |
|---|---|---|
compiler | The compiler itself | Semver for the tool |
syntax | Directives, expression grammar, script boundary | Within a major, every component that compiled keeps compiling with the same meaning |
output | Generated module surface: mountComponent, { setProps?, destroy() }, hydration and server exports | Within a major, modules keep their surface and behavior |
Security boundaries
- Components are trusted code. Wizz is not a sandbox; never compile user-submitted
.wizzsource. - Client text is created with
createTextNode(); static values are serialized into the module rather than parsed as HTML. - Server output escapes every text and attribute value, and the state script escapes every
<so state cannot close the tag early. - Hydration never uses
innerHTMLoreval— adoption is a read-only DOM walk. - URLs in dynamic
href/srcattributes need application-level validation when they come from untrusted data. - Source maps embed the original component source; keep
.mapfiles private if that matters.