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.
What it transforms
Section titled “What it transforms”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.
| Source | Transformed via phaze-compiler | What it saves you |
|---|---|---|
c(expr) from /dsl | c(() => expr) (auto-thunked) | The () => ceremony at every computed declaration |
watch(expr) from /dsl | effect(() => expr) (auto-thunked + alias) | Same, plus the effect → watch rename for readability |
phaze(expr) from /dsl | (() => expr) (macro — the import drops out) | Reactive child-expressions without writing the arrow |
s.async(expr) from /dsl | s.async(() => expr) (auto-thunked) | Async-loader thunk ceremony at every s.async declaration |
inc(sig) / dec(sig) / add(sig, n) / sub(sig, n) from /numeric | sig.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 /time | interval(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 /time | interval(() => { 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.”
What it diagnoses
Section titled “What it diagnoses”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:NAMEwithNAMEnot in scope — build-time error pointing at the missing import or theon:-vs-use:namespace mistake.phaze:onXxx={fn}— build-time error suggestingon:click={fn}(the naïve compile would wire a getter that never fires the handler).bind:value/bind:checkedon 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 runtimeconsole.warnwith the bind-to-const fix. Dead-strips in production viaimport.meta.env.DEVgating.
Where it runs
Section titled “Where it runs”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.
Why every transform is build-time
Section titled “Why every transform is build-time”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.
Where to look next
Section titled “Where to look next”- 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-astrowires 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.