DSL & directives
DSL stands for Domain Specific Language — the DX layer of Phaze, enabled in God Mode.
In Phaze, directives can be thought of as a “headless component” that adds reactive behavior to a DOM element. The core directives phaze:, on:, class:, bind: don’t add any additional size — use: directives can be thought of as “add-ons”. Directives stack and keep things much cleaner. Phaze has been built with performance from the ground up — directives are tree-shaken per-import, so you pay only for the use: directives you attach. For caching granularity in production, directives can also be split into their own chunk via chunkDirectives: true, bundled as phaze-directives.js.
The DSL subpath — @madenowhere/phaze/dsl
Section titled “The DSL subpath — @madenowhere/phaze/dsl”Four exports, all compile-time-aware:
| Name | Aliases | What the phaze-compiler does |
|---|---|---|
s | signal | Plain alias — no transform |
c | computed | Auto-thunks: c(expr) → c(() => expr) |
watch | effect | Auto-thunks: watch(expr) → watch(() => expr) |
phaze | (macro) | Rewrites phaze(expr) → () => expr, drops the import |
import { s, c, watch, phaze } from '@madenowhere/phaze/dsl'import { cleanup } from '@madenowhere/phaze'
export function Counter() { const count = s(0) const doubled = c(count() * 2) // auto-thunked watch(console.log(`count: ${count()}`)) // auto-thunked cleanup(() => console.log('unmounted')) // imported normally
return ( <button on:click={() => count.update(n => n + 1)}> {count} × 2 = {doubled} </button> )}Equivalent without the DSL:
import { signal, computed, effect, cleanup } from '@madenowhere/phaze'
const count = signal(0)const doubled = computed(() => count() * 2)effect(() => console.log(`count: ${count()}`))The two are transformed by the phaze-compiler to the same code. The DSL is purely an authoring convention — pick the import style your project prefers.
The phaze() macro — reactive child expressions
Section titled “The phaze() macro — reactive child expressions”Bare signal reads in JSX ({count}) are the fast path: the runtime detects the callable and wires it to a text/attribute binding directly. The moment you need to do something to the value — arithmetic, formatting, comparison — wrap in phaze():
<p>{count}</p> {/* bare — fast path */}<p>{phaze(`Total: $${total()}`)}</p> {/* template literal */}<p>{phaze(count() > 5 ? 'high' : 'low')}</p> {/* expression */}phaze(expr) rewrites to () => expr; the import drops out and the macro body tree-shakes. Same bytes as writing the arrow yourself.
The phaze-compiler also auto-wraps ternaries and && expressions in JSX child position, so {cond() ? <A/> : <B/>} is reactive without phaze(). The wrapper is for cases the auto-wrap doesn’t cover — most commonly template literals.
Subpath gotcha
Section titled “Subpath gotcha”import { signal as s } from '@madenowhere/phaze' is not the same as import { s } from '@madenowhere/phaze/dsl'. The first is a plain rename; the second activates phaze-compile‘s auto-thunking. The transform is keyed on the import path, not the local name.
JSX namespaces
Section titled “JSX namespaces”Five prefixes, each owning one job:
| Namespace | Purpose | Transformed via phaze-compiler |
|---|---|---|
phaze:attr={expr} | Reactive attribute | attr={() => expr} |
on:event={fn} | Native DOM event listener | onEvent={fn} (camelCase); CallExpression values auto-thunk to () => … |
use:name={value} | Behavior directive | name(el, () => value) post-creation |
class:name={cond} | Conditional class toggle | effect(() => el.classList.toggle('name', !!cond)) post-creation |
bind:value={signal} / bind:checked={signal} | Two-way input binding (minimal scope) | Signal pass-through + listen() post-creation |
Combining everything
Section titled “Combining everything”All five namespaces compose on a single element. Post-creation operations (use:, class:, bind:) run in source order inside one IIFE; phaze: and on: rewrite in place to standard attributes:
<input type="text" class="input" use:autofocus={true} class:invalid={!valid()} bind:value={text} on:focus={track}/>Transformed via phaze-compiler (approximate)
((__el) => ( autofocus(__el, () => true), effect(() => __el.classList.toggle('invalid', !valid())), listen(__el, 'input', (e) => text.set(e.currentTarget.value)), __el))(<input type="text" class="input" value={text} onFocus={track} />)The phaze:/on:/use:/class:/bind: namespaces and the DSL auto-thunking require phaze-compile in your build pipeline.
- Astro:
@madenowhere/phaze-astrowires the phaze-compiler automatically. No extra config. - Vite (no Astro): add the
@madenowhere/vite-plugin-phazeplugin, which includes the phaze-compiler. - Other bundlers: import the Babel plugin from
@madenowhere/phaze-compiledirectly.
If you only want the runtime — no DSL, no namespaces — @madenowhere/phaze alone works with any tooling that supports the standard JSX automatic transform. See Setup.
In this section
Section titled “In this section”- DSL: Attributes & events —
phaze:attrreactive attributes andon:eventnative DOM events - DSL: Directives —
use:directives,use:spring,use:warp, directive anatomy, and the core directives package - DSL: Binding & transitions —
class:,bind:,defer:, andtransition: - DSL: Diagnostics — compile-time errors, runtime warnings, and bundle impact