Under the Hood

This page is for the enthusiasts. Nothing here is required to use JAF - the point of the machinery below is that you never think about it. But if you want to know what happens between the .html file you write and the pixels on screen, this is the tour.

The one-sentence version: JAF is a compiler attached to a very small runtime. Your components are compiled twice - the <script> through an AST transform, the markup through a template compiler that emits real DOM construction code - and what ships is ordinary JavaScript modules. No HTML parsing at runtime, no interpreter, no virtual DOM.

The compiler: Rust for the script, JAF for the markup

The <script> block is parsed and rewritten at the AST level by SWC, the Rust compiler toolchain. let count = 0 becomes a registered reactive signal; every read inside an expression becomes a tracked read; every write becomes an invalidation that notifies exactly the bindings that depend on it. const stays static. This is why JAF has no useState, no ref(), no $: labels - the compiler sees let and does the rest.

Picking SWC puts JAF in unusual company. Most framework compilers are pure JavaScript - Svelte's, Vue's, Solid's Babel transform, even the new React Compiler - which keeps them portable at the cost of speed. The Rust camp is mostly toolchains: Next.js's entire compiler is SWC plugins, and Parcel, Deno and Rspack build on it. The only other framework whose own compiler sits on SWC is Qwik. JAF takes the same trade: Rust-speed transforms on every build, and a WASM build of SWC for the one place a browser has to run the compiler itself (the REPL).

The markup goes through JAF's own template compiler. A component like this:

source
<div class="counter">
	<span>Count: {count}</span>
	<button onclick="count++" class:active={count> 0}>+</button>
</div>

<script>
	let count = 0
</script>

compiles to a module shaped like this (simplified):

import { template } from "jafjs/runtime/dom/template"
import { insert } from "jafjs/runtime/dom/insert"
import { toggleClass } from "jafjs/runtime/dom/classList"
import { on } from "jafjs/runtime/dom/on"

const _tmpl0 = template(`<div class="counter"><span>Count: <!--></span><button>+</button></div>`)

const _ev0_0 = (ctx) => ctx.count
const _ev0_1 = (ctx) => ctx.count > 0

function _render0(ctx) {
  const _r  = _tmpl0()            // cloneNode(true)
  const _e0 = _r.firstChild       // div.counter
  const _e1 = _e0.firstChild      // span
  const _a0 = _e1.childNodes[1]   // the comment anchor
  const _e2 = _e1.nextSibling     // button

  insert(_a0, () => _ev0_0(ctx))
  toggleClass(_e2, "active", () => _ev0_1(ctx))
  on(_e2, "click", ctx.$handlers[0])
  return _r
}

function _setup0(jaf) {
  let count = jaf.reactive("count", 0)
  jaf.handler(0, () => { count.set(count.get() + 1) })
}

export const component0 = { template: _tmpl0, render: _render0, setup: _setup0, meta: {...} }

Three properties of this shape do most of the work:

  • The template parses once. Every mount after the first is a cloneNode(true), roughly an order of magnitude cheaper than innerHTML.
  • Element access is a compiled path walk - firstChild, nextSibling. No querySelector, no marker attributes. The DOM ships clean.
  • Bindings are closures, not strings. Each {expression} is a tiny compiled function. Nothing is parsed, interpreted or eval'd in the browser.

Reactivity: signals, deep, batched

The runtime half is a fine-grained signal system. A binding subscribes to exactly the values it read; a write notifies exactly those bindings. There is no diffing pass and no re-render - an update is the handful of closures whose inputs changed.

Tracking is deep by default: push to an array, assign a nested field, and the bindings that read it update. That convenience is the product; for the rare hot list where per-row proxies matter, {@static ...} opts out. Writes in one task batch into one update, effects carry automatic cleanup, and timers and listeners registered in a component die with it.

Templates all the way down

A component stops being one template the moment it contains control flow. Every <show> arm, <for> row body, overlay body and piece of projected slot content becomes a unit: its own template(), its own render function, compiled by the same emitter recursing into itself. In the parent, each unit is a single comment anchor; runtime helpers render the right unit at the right anchor and own the resulting range - when a branch flips or a row leaves, exactly that range is disposed.

  • Lists are keyed by identity. Rows whose keys survive keep their exact DOM nodes - asserted in the test suite by reference equality. An <input> in a row keeps focus and its half-typed value while rows shuffle around it.
  • Slot scope is lexical by construction. Content you write in the parent compiles as a unit of the parent, so its bindings close over the parent's state - there is no runtime ownership protocol to get wrong. A slot that offers values passes them as an argument to that unit, typed, reaching both component tags and expressions in the content.
  • Components are imports, not lookups. A child tag resolves at build time to a module reference the bundler settled. No runtime registry, no name collisions at runtime, and no wrapper element - a component contributes its own nodes, so it works inside a <table>, a flex row, and around position: fixed.

React components without React

JSX islands are compiled by JetShake, a sister compiler that turns React-API components into signals and direct DOM - no React runtime ships. A compiled island mounts at an anchor like any other unit, and props cross as compiled thunks driving JetShake signals from JAF effects. A keyed .map() of JSX rows compiles to a hosted list module with JetShake's keyed reconciler behind it. The practical upshot: the React component ecosystem is available, at single-digit kilobytes.

Routes, layouts, state

Routing is file-based and compiled like everything else: pages and layouts are components, layout inheritance chains are resolved at build time, and a route group's records are placed in the chunk that group loads. Guards compile their denial destination in. State channels - localStorage, session, URL query and path segments, cross-file unite(), the global store - are declared in the script and wired by the compiler; the runtime keeps only the channels your app uses, because the build computed the bill.

HTML comes back out

A second backend renders the same emitted modules to strings with no document, held equal to the DOM backend by equivalence tests. JAF_PRERENDER=1 vite build writes static routes as real HTML - scoped CSS included - and the client adopts that markup instead of wiping it. Opt-in in 0.1, with its limits documented; it is also the foundation the future SSR story stands on.

The honesty rule: named errors

The compiler's rule is: express it fully, or refuse it by name. Everything in the framework compiles; when the compiler meets something it cannot express - a typo'd component name, an unterminated block - the build stops with an error naming the component and the reason, grouped by root cause. There is no silent middle ground, and the test suite pins every message. That includes source a visitor typed: the REPL on this site runs this same compiler in the browser, so a component it refuses is refused in the words the build would have used, and one it accepts is the module the build would have written.

Security is a compile artefact

A compiled app contains no eval, no new Function, and no inline handlers - listeners are attached with addEventListener from module code. It runs under script-src 'self'. That is not a hardening pass; it is what the output is made of. Bound URL attributes are guarded against javascript: on every backend.

How we know it is correct

Three tiers hold the machinery honest. Parity suites drive both the compiled path and the interpreter through the same mounts and assert identical DOM, before and after interaction. A golden corpus of real pages is compiled on every test run with each emitted module pinned as a snapshot. And a booted tier builds real production bundles, boots them in a DOM, clicks buttons and reads the result - the tier that has caught every bug the others could not see, including two found in the framework's own test week that had shipped quietly for days.

What all this buys you

  • Bytes - hello-world lands under 10 kB gzipped, and a build includes only the helpers your templates call.
  • Speed - clone-and-walk mounting, fine-grained updates with no diffing, keyed lists that move nodes instead of rebuilding.
  • Strict CSP - by construction, not configuration.
  • Honest failure - a build error with a name beats a page that quietly renders nothing.

If the architecture sounds familiar, it should: compiled templates and fine-grained reactivity are where the fastest frameworks converged - Solid's compiled JSX, Svelte's framework-as-compiler, Qwik's Rust optimizer. JAF's contribution is applying it to plain .html files: you write HTML, and the compiler meets you there.