Skip to content

Phaze Compiler

@madenowhere/phaze-compile is the build-time JSX/AST transformer that turns the ergonomic Phaze surface (s / c / watch / phaze DSL aliases, the phaze: / on: / use: / class: / bind: JSX namespaces) into the same code you’d write by hand against the plain Phaze runtime. Every transform runs at compile time. Nothing it does adds a byte to your shipped bundle.

It’s a Babel plugin under the hood, paired with a Vite plugin entry that wires it into the Astro/Vite pipeline automatically. The plugin runs before the standard JSX-to-jsx() transform — it produces JSX AST output, which esbuild/swc/babel-jsx-transform then converts to function calls.

Eight categories of transform. Each one is documented in detail in DSL & directives; this page is the one-liner-per-transform index plus the why-it-matters.

SourceTransformed via phaze-compilerWhat it saves you
c(expr) from /dslc(() => expr) (auto-thunked)The () => ceremony at every computed declaration
watch(expr) from /dsleffect(() => expr) (auto-thunked + alias)Same, plus the effectwatch rename for readability
phaze(expr) from /dsl(() => expr) (macro — the import drops out)Reactive child-expressions without writing the arrow
s.async(expr) from /dsls.async(() => expr) (auto-thunked)Async-loader thunk ceremony at every s.async declaration
inc(sig) / dec(sig) / add(sig, n) / sub(sig, n) from /numericsig.set(sig() ± n) (inlined, import declaration drops out)The n => n + 1 updater-function ceremony on number signals; zero bytes shipped from /numeric after compilation
interval(delay, expr) / timeout(delay, expr) from /timeinterval(delay, () => expr) (second-arg auto-thunked)Tick-callback () => ceremony; lets interval(1000, inc(count)) work as written
interval(restart, delay, fn) / timeout(restart, delay, fn) from /timeinterval(() => { restart(); return delay }, () => fn) (3-arg sugar; import-time-only, no runtime fallback)The full () => { signal_read(); return ms } ceremony for debounce-shaped patterns; turns timeout(draft, 1000, saveDraft()) into the canonical form at build. Misuse (literal restart, function-shaped delay, spread args) is caught with descriptive compile-time errors.
phaze:attr={expr}attr={() => expr}Reactive attribute bindings via plain JSX prop syntax
on:event={fn}onEvent={fn} (camelCase rename)Visual alignment with the other namespaces
on:event={callExpr}onEvent={() => callExpr} (auto-thunked, DEV-only factory warning injected)Inline event handlers as bare expressions: on:click={state.set('s2')}
use:NAME={value}((__el) => (NAME(__el, () => value), __el))(<jsx/>) (post-creation IIFE)Behavior directives attached to elements without ref ceremony
use:spring={IDENT[KEY]} (sibling springs key present)Same IIFE, value rewritten in-place to { to: IDENT[KEY], springs: IDENT.springs } (auto-fuse)State-machine spring configs as one record + one JSX line
class:NAME={cond}effect(() => __el.classList.toggle('NAME', !!cond)) (post-creation)Conditional class toggles via JSX-native syntax; effect import auto-injected
bind:value={signal} (text-like inputs / textarea)value={signal} pass-through + onInput={(e) => signal.set(e.currentTarget.value)}Two-way binding for the trivial cases, compile-error for the non-trivial ones
bind:checked={signal} (checkbox)checked={signal} pass-through + onChange={(e) => signal.set(e.currentTarget.checked)}Same, for checkboxes
for:NAME={signal} on <For>each={signal} + children wrapped in (NAME) => …Per-item binding declared in one attribute; lifts inner key={…} to getKey automatically
<For for:item={items}>…</For> (no phaze/getKey){() => items().map((item) => …)} (inversion; For import drops when every <For> in the file is inversion-eligible)Reactive list, SSR-renders every row, zero shipped bytes. The phaze attribute opts into the runtime For (~900 B brotli) when row identity has to survive reorders — see API › <For>.
JSX children of a component (<Catch><App/></Catch>)<Catch>{() => <App/>}</Catch> (auto-wrapped)Flow components (<Catch>, <Switch>, <Portal>, <Dynamic>) work without explicit thunks at every call site

The full list with per-transform examples lives in DSL & directives. This table is for at-a-glance “what does the compiler actually do.”

phaze-compile also catches compile-time errors and emits DEV-only runtime guards for the common phaze footguns. Both are designed to surface mistakes loudly with the fix in the message — see DSL & directives → Diagnostics for the full catalogue.

Highlights:

  • use:NAME with NAME not in scope — build-time error pointing at the missing import or the on:-vs-use: namespace mistake.
  • phaze:onXxx={fn} — build-time error suggesting on:click={fn} (the naïve compile would wire a getter that never fires the handler).
  • bind:value / bind:checked on incompatible elements — build-time error with the manual-form fallback in the message (covers <input type="number">, <select>, <input type="radio">, etc.).
  • on:click={callExpression} returning a function — DEV-only runtime console.warn with the bind-to-const fix. Dead-strips in production via import.meta.env.DEV gating.

phaze-compile is invoked by @madenowhere/phaze-vite, which is invoked by @madenowhere/phaze-astro’s Astro integration. The plugin chain:

your .tsx / .mdx → phaze-compile (babel) → esbuild JSX → vite output
wires in via phaze-astro / phaze-vite at "astro:config:setup"

You don’t import phaze-compile in your component files. Adding phaze() to integrations in astro.config.mjs (or registering the Vite plugin equivalent for non-Astro projects) is the only setup step.

The runtime cost of every transform on this page is zero. Phaze’s runtime knows nothing about the DSL aliases, the JSX namespaces, or the directive IIFE shape — by the time the runtime sees the code, it’s already plain jsx() calls and plain function-typed JSX prop values that the runtime’s existing fast paths handle. The compiler’s job is to write the boilerplate so you don’t have to; the runtime’s job is unchanged from the no-compiler form.

This is why phaze (the runtime as shipped to the client) stays under 3 KB brotli — every layer of ergonomics is paid for at build time, not at startup.

  • DSL & directives — full per-transform documentation with side-by-side examples (the user-side JSX + the compiled output in <details> blocks).
  • Diagnostics — the table of every compile-time error + DEV/SSR-gated runtime warning the compiler ships.
  • Astro setup — how @madenowhere/phaze-astro wires phaze-compile into the Vite/Astro pipeline.
  • Bundle impact — per-transform byte-delta-vs-handwritten ledger.

If you’re building a phaze-aware library (something that consumes phaze internals — signal / effect / cleanup), see Phaze as a peer dependency — that’s the contract for declaring phaze without bundling a second runtime instance.