.phaze: Examples & pipeline
Full example — page
Section titled “Full example — page”Uses the canonical stacked form (no closing --- between boundaries — each ---<label> implicitly closes the previous; one bare --- at the end transitions to the trailing region).
---pageimport { hours, days } from '@madenowhere/phaze/time'import ProductLayout from '../../components/ProductLayout'
layout: ProductLayouthead: { description: 'Browse all products in our catalog.' }revalidate: { maxAge: hours(1), swr: days(1), tags: ['products'] }---dataproducts: env.DB.list()featured: env.DB.list({ category: 'featured', limit: 3 })---stateimport { s, c } from '@madenowhere/phaze/dsl'import { is, not } from '@madenowhere/phaze/match'import { withRevalidate } from '@madenowhere/phaze/revalidate'
const filterMode = s<'all' | 'new' | 'sale'>('all')const activeUsers = s.async(fetch('/api/active-users').then(r => r.json()))withRevalidate(activeUsers, 30)---
const state = s<'s1' | 's2' | 's3'>('s1')
const SECTION_ANIM = { s1: { y: 0, opacity: 1 }, s2: { y: -8, opacity: 0.6 }, s3: { y: 0, opacity: 1 }, springs: { y: { stiffness: 500, damping: 100, mass: 3 }, opacity: { stiffness: 300, damping: 60 }, },}
<> <title>{`${data.products.length} products — Neuralkit`}</title>
<header class="page-header"> <h1>Products</h1> <small>{activeUsers.value()?.count ?? '…'} shoppers online</small> </header>
<nav class="filter-bar"> <button on:click={filterMode.set('all')} class:active={is(filterMode, 'all')}>All</button> <button on:click={filterMode.set('new')} class:active={is(filterMode, 'new')}>New</button> <button on:click={filterMode.set('sale')} class:active={is(filterMode, 'sale')}>Sale</button> </nav>
<section use:spring={SECTION_ANIM[state()]} on:mouseenter={state.set('s2')} on:mouseleave={state.set('s3')} class:loaded={not(state, 's1')} > {data.products .filter(p => is(filterMode, 'all') || p.category === filterMode()) .map(p => ( <article> <h3>{p.name}</h3> <span>${p.price}</span> </article> ))} </section></>Full example — component (non-page)
Section titled “Full example — component (non-page)”import type { JSXChild } from '@madenowhere/phaze'import { s } from '@madenowhere/phaze/dsl'import { is, not } from '@madenowhere/phaze/match'import { spring } from '@madenowhere/photon/phaze'import { actions } from 'phaze:transport'
---data{/* Module-scope — shared across every Card instance on the page. */}const commentCounts = s.async(actions.getAllCommentCounts())---state{/* Module-scope UI state — toggled once, every Card sees it. */}const focusMode = s<'all' | 'unread'>('all')---
({ post }: { post: { id: string; title: string; excerpt: string } }) => { const state = s<'s1' | 's2' | 's3'>('s1')
const CARD_ANIM = { s1: { y: 0, opacity: 0.6, scale: 1 }, s2: { y: -4, opacity: 1, scale: 1.02 }, s3: { y: 0, opacity: 1, scale: 0.98 }, springs: { y: { stiffness: 500, damping: 100, mass: 3 }, opacity: { stiffness: 300, damping: 60 }, scale: { stiffness: 400, damping: 80 }, }, }
return ( <article use:spring={CARD_ANIM[state()]} on:mouseenter={state.set('s2')} on:mouseleave={state.set('s1')} on:mousedown={state.set('s3')} on:mouseup={state.set('s2')} class="card" class:hovered={not(state, 's1')} class:active={is(state, 's3')} class:focused={is(focusMode, 'unread')} > <h3>{post.title}</h3> <p>{post.excerpt}</p> <small>{commentCounts.value()?.[post.id] ?? 0} comments</small> </article> )}Compile pipeline
Section titled “Compile pipeline”.phaze source │ ▼phaze-compile pre-processor ← state machine: extracts boundaries │ (---page, ---data, ---state + trailing) │ ▼synthetic .tsx ← multi-export shape, JSX trailing function │ ▼phaze-compile babel passes ← DSL strip, match/numeric/list/time strip, │ extractSlots, staticSubtreeHoist, defer:, │ transition:, class:, bind:, on:event, │ <title> lift to head, all unchanged ▼emitted JSX ← same as hand-written .tsx output │ ▼esbuild JSX transform ← same downstream pipeline │ ▼Rollup chunking ← phazeChunks() rules apply unchanged │ ▼shipped bundlesArchitectural moat: the pre-processor emits a synthetic .tsx and hands it to the existing babel pass chain. Every present and future phaze-compile pass works inside .phaze automatically — no duplication, no maintenance fork.
Tooling — host adapter wiring
Section titled “Tooling — host adapter wiring”.phaze is not a runtime concept; it’s wired into the build by the host adapter (@madenowhere/phaze-cloudflare, future @madenowhere/phaze-*). End users don’t configure anything — installing the adapter is enough. This section documents what the adapter does so authors of new adapters can mirror it, and so users debugging a misconfigured project know what to look for.
Three integration points, all in the host adapter’s Vite plugin:
1. Load hook — claim ownership of .phaze paths. Rollup’s default loader errors out on unknown extensions before any transform hook runs. The adapter’s load hook reads .phaze files off disk and returns the source as-is. (phaze-compile itself stays file-system-agnostic — it only owns transform, not load. Host adapters already have Node-side fs machinery for routing, so that’s where the read lives.)
async load(id) { const path = id.split('?')[0] ?? id if (/\.phaze$/.test(path)) { try { return readFileSync(path, 'utf8') } catch { return null } } // …other virtual modules}2. esbuild filter expansion — recognize .phaze for the JSX→jsx() pass. phaze-compile’s transform produces JSX-shaped output (component children hoisted to thunks; the JSX itself preserved for esbuild to lower). Vite’s built-in vite:esbuild plugin handles that lowering downstream, but its default filter (/\.(m?ts|[jt]sx)$/) skips .phaze. The adapter expands the filter via config.esbuild.include.
3. loader: 'tsx' pin — required when the filter includes a non-standard extension. This is the gotcha. Vite’s transformWithEsbuild derives the loader from path.extname(filename).slice(1) when options.loader is unset — for foo.phaze that yields the literal string 'phaze', which esbuild rejects with Invalid loader value: 'phaze'. Pinning the loader forces every filter-matched file through the tsx loader (a superset of ts; correct for both .tsx and .phaze outputs).
// inside the adapter's vite plugin `config()` hookreturn { esbuild: { jsx: 'automatic', jsxImportSource: '@madenowhere/phaze', include: /\.(m?ts|[jt]sx|phaze)$/, loader: 'tsx', // ← the pin }, // …}This pattern is the upstream-canonical Vite 7 recipe for custom file extensions (vitejs/vite#7321) — the same shape Astro uses to wire .astro, Vue uses for .vue, and Svelte uses for .svelte. It’s documented here (rather than left as a discovery problem) because the failure mode is opaque: a user adding .phaze support to a custom build, or an adapter author copying only the first two integration points, hits the Invalid loader value: 'phaze' error with no obvious connection to esbuild’s per-file loader inference.
Adapters that spawn a transient Vite server (e.g. for prerender) must restate the same esbuild block on that server too — the second instance doesn’t inherit the main plugin’s config. phaze-cloudflare’s runPrerender does this; future adapters following the same pattern should mirror it.
TypeScript LSP
Section titled “TypeScript LSP”.phaze editor support uses Volar — the same framework Vue, Astro, and MDX use for custom-format LSP integration. The Volar adapter runs the same .phaze → .tsx pre-processor used by the build, exposes the synthetic .tsx to the TypeScript language server, and maps positions back to .phaze source via a sourcemap.
This gives .phaze files the full TypeScript LSP feature set for free — completion, hover, find-references, rename, codemods, refactors — without phaze maintaining its own language server. The pattern is well-trodden; the adapter is ~200–400 LoC of glue.