@madenowhere/phaze-astro
v0.0.9
Published
Astro renderer for Phaze. SSR + hydration for `.tsx` islands, all client:* directives, useAction() helper for Astro Actions.
Downloads
811
Readme
@madenowhere/phaze-astro
Astro renderer for Phaze — drop into any Astro project to render .tsx islands with Phaze's runtime.
Install
pnpm add @madenowhere/phaze-astro phaze// astro.config.mjs
import { defineConfig } from 'astro/config'
import phaze from '@madenowhere/phaze-astro'
export default defineConfig({
integrations: [phaze()],
})That's the whole config. The integration registers Phaze's renderer, wires the @madenowhere/phaze-compile Vite plugin (so JSX gets compiled to template() + bindings), and configures TypeScript JSX import-source to resolve to phaze.
Client directives
Every Astro client directive works:
| directive | when it hydrates |
|---|---|
| client:only="phaze" | mounts in the browser only — no SSR HTML at all |
| client:load | SSR'd HTML embedded in the page, hydrated as soon as the JS loads |
| client:idle | SSR'd, hydrated when the main thread is idle |
| client:visible | SSR'd, hydrated when scrolled into view |
| client:media={"(min-width: 720px)"} | SSR'd, hydrated when the media query matches |
---
import Counter from '../components/Counter.tsx'
---
<Counter client:load start={42} />The component runs once at hydration via Phaze's hydrate() (which adopts the SSR'd DOM rather than rebuilding it), or via render() for client:only.
useAction — Astro Actions as signal state
Astro Actions are end-to-end-typed RPC. @madenowhere/phaze-astro/actions wraps the action call shape in pending/error/data signals so islands don't have to roll the dance themselves:
import { signal } from 'phaze'
import { actions } from 'astro:actions'
import { useAction } from '@madenowhere/phaze-astro/actions'
export default function SaveButton({ scene }) {
const save = useAction(actions.saveScene)
return (
<button
disabled={save.pending}
onClick={() => save.execute({ id: 'hero', data: scene() })}
>
{() => (save.pending() ? 'saving…' : save.error() ? 'retry' : 'save')}
</button>
)
}useAction returns:
pending—Signal<boolean>. True while a call is in flight.error—Signal<unknown>. Last error, or null. Cleared on the next successful call.data—Signal<TData | undefined>. Last successful payload.execute(input)— fires the action; returns the same{ data, error }shape Astro returns directly, so callers can branch on the result inline.
Concurrent calls leave pending true until the last resolves; whichever resolves last wins for data.
Cross-island signal sharing
Astro renders each island into its own DOM tree, but the JS modules they import are shared by Vite's bundler — so a signal exported from a shared module is the same identity across islands on the same page:
// src/state.ts
import { signal } from 'phaze'
export const cartItems = signal<Item[]>([])// src/components/CartIcon.tsx
import { cartItems } from '../state'
export default () => <span>{() => cartItems().length}</span>// src/components/AddToCart.tsx
import { cartItems } from '../state'
export default ({ item }) => (
<button onClick={() => cartItems.update((items) => [...items, item])}>
add
</button>
)Drop both islands on a page; clicking AddToCart updates CartIcon reactively. No store library, no event bus — Phaze's runtime is shared between the islands.
This works because Vite dedupes the phaze package across island bundles. The framework instance is the same, so signal identity is preserved.
Limitations to know
- Hydration is best-effort root adoption today. The root container is adopted but children are recreated as the JSX evaluates leaf-first. Full identity-preserving hydration ships with the M3 lazy-children compile pass. For most apps this is invisible; for apps with mid-flight CSS animations on hydrating elements, expect a brief reset.
- Some dev-mode flicker on cold start. Vite's optimizer late-discovers
astro/actions/runtime/entrypoints/client.jsand emits a one-time reload.astro previewis unaffected. See GAPS.md §0 for the full diagnosis and the workaround we don't apply (because the canonical workflow isdevfor editing,previewfor verifying). - No streaming SSR for slow island loaders yet. Resolve loader data in the page frontmatter before render, or defer to a
client:loadisland that fetches client-side.
See GAPS.md for a running list of issues that would be cleaner if fixed upstream in Astro / @astrojs/cloudflare rather than worked around in this layer.
Example
examples/astro-cloudflare/ — a KV-backed todo app showing all of the above end-to-end (Astro Actions + useAction + Cloudflare KV + Tailwind).
