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:

src/compiler/index.js
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 }
  1. 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 let and let declarations and persist() markers.
  2. 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.
  3. 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:

Counter.wizz
<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:

dist/components/Counter.js (simplified)
// 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, and disabled as 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).

application-owned delivery
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;
  1. wizz dev and the node adapter match the URL against the route manifest and call renderComponent() per request.
  2. The router finds the application/wizz-state script beside #app, imports the hydratable build, and calls hydrateComponent().
  3. The hydration walk verifies tags, data-wizz-ids, and text against the template, then attaches listeners. Child components are adopted in place through hydrateRoot().
  4. On any mismatch it logs one [wizz] hydration mismatch warning, 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:

dist/App.js:1
// Generated by Wizz 1.17.0 (component syntax 1.4.1, generated output 1.8.2). Edits will be overwritten.
VersionCoversGuarantee
compilerThe compiler itselfSemver for the tool
syntaxDirectives, expression grammar, script boundaryWithin a major, every component that compiled keeps compiling with the same meaning
outputGenerated module surface: mountComponent, { setProps?, destroy() }, hydration and server exportsWithin a major, modules keep their surface and behavior

Security boundaries

  • Components are trusted code. Wizz is not a sandbox; never compile user-submitted .wizz source.
  • 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 innerHTML or eval — adoption is a read-only DOM walk.
  • URLs in dynamic href/src attributes need application-level validation when they come from untrusted data.
  • Source maps embed the original component source; keep .map files private if that matters.