4. phaze-astro
1. phaze-tsplugin ← editor (TS Language Service)2. phaze-compile ← build-time AST rewriting ├── babel-plugin.ts ← the actual Babel plugin (all the visitors live here) └── vite-plugin.ts ← thin wrapper that adapts it for Vite3. phaze-vite ← island HMR + chunking helpers4. phaze-astro ← Astro integration (island model) ← you are here5. phaze-cloudflare ← native Cloudflare Workers adapter (whole-page)@madenowhere/phaze-astro is the tool you actually install when you want to use Phaze in an Astro app. It’s the highest-level layer in the tooling stack — its job is to register Phaze as a JSX renderer with Astro and to pull in the lower layers (phaze-compile, phaze-vite) so the app author doesn’t have to wire them up by hand.
If you’re building a non-Astro Vite app, you don’t need this — you use phaze-compile + phaze-vite directly. phaze-astro is specifically the Astro framework glue.
What an Astro integration is
Section titled “What an Astro integration is”Astro integrations are plugins that hook into Astro’s build lifecycle via named hooks: astro:config:setup, astro:server:setup, astro:build:done, etc. Each hook gives the integration access to Astro’s internal config + Vite config, lets it call helper APIs like addRenderer() to register a JSX runtime, and lets it call updateConfig() to inject Vite plugins or modify build settings.
phaze-astro implements one hook: astro:config:setup. Everything it needs to do happens at config-resolution time, before Astro starts the dev server or build:
// packages/astro/src/index.ts (abbreviated)export default function phazeIntegration() { return { name: '@madenowhere/phaze-astro', hooks: { 'astro:config:setup': ({ addRenderer, updateConfig }) => { // 1. Register the Phaze JSX renderer addRenderer({ name: '@madenowhere/phaze-astro', clientEntrypoint: '@madenowhere/phaze-astro/client', serverEntrypoint: '@madenowhere/phaze-astro/server', })
// 2. Wire phaze-compile into Vite's plugin chain updateConfig({ vite: { plugins: [phazeCompile()], esbuild: { jsx: 'automatic', jsxImportSource: '@madenowhere/phaze', }, optimizeDeps: { exclude: ['phaze', 'phaze/jsx-runtime', 'phaze/store'] }, ssr: { noExternal: [ '@madenowhere/phaze', '@madenowhere/phaze-astro', '@madenowhere/phaze-render-to-string', '@madenowhere/phaze-compile', ], }, }, }) }, }, }}Two responsibilities — both happen at the astro:config:setup hook:
Responsibility 1 — Register the JSX renderer
Section titled “Responsibility 1 — Register the JSX renderer”addRenderer({...}) tells Astro: “when a JSX-shaped island (<MyComponent client:load/>) needs to render, here are the modules you call.” Astro uses framework renderers to support React, Preact, Vue, Solid, Lit, and now Phaze — all interchangeable via the same hook.
clientEntrypoint: '@madenowhere/phaze-astro/client'— Astro loads this in the client bundle forclient:load/client:idle/client:visible/client:mediaislands. The entrypoint exports a function that mounts a Phaze component into the island’s container, pickinghydratevsrenderbased on whether the container has SSR’d children.serverEntrypoint: '@madenowhere/phaze-astro/server'— Astro loads this during SSR (when generating HTML forclient:loadand friends). The entrypoint exports arenderToStaticMarkupfunction that delegates to@madenowhere/phaze-render-to-string’s real SSR renderer.
Once registered, every .tsx file in the project compiles through Phaze’s JSX runtime, every island mounts via Phaze’s render/hydrate, every SSR island renders to HTML via the linkedom-backed renderToString. The framework slot Astro provides is filled completely by Phaze without app-author code.
Responsibility 2 — Wire phaze-compile into Vite
Section titled “Responsibility 2 — Wire phaze-compile into Vite”updateConfig({ vite: { plugins: [phazeCompile()], ... } }) injects phaze-compile’s Vite plugin into Vite’s plugin chain. After this runs, every .tsx/.jsx file in your project goes through babel-plugin.ts in phaze-compiler before reaching esbuild’s JSX-to-jsx() pass.
The accompanying config:
| Key | Why |
|---|---|
esbuild.jsx: 'automatic' | Tells esbuild to use the automatic-runtime JSX form (import jsx/jsxs/Fragment automatically), not the classic React.createElement form. |
esbuild.jsxImportSource: '@madenowhere/phaze' | The package whose /jsx-runtime subpath provides those auto-imported jsx/jsxs/Fragment functions. Phaze ships these. |
optimizeDeps.exclude | Stops Vite from pre-bundling phaze (workspace ESM; pre-bundling races with workspace rebuilds). |
ssr.noExternal | Forces Vite to run phaze packages through its SSR transform — without this, Astro 6’s default externalization loads them as raw Node ESM, where import.meta.env.DEV is undefined and the first reactive primitive crashes. |
App authors writing astro.config.mjs see this as a single line:
import phaze from '@madenowhere/phaze-astro'
defineConfig({ integrations: [phaze()], // ← does all of the above behind one call})That one call is the integration story. Everything from babel-plugin.ts in phaze-compiler being in the Vite pipeline to JSX islands rendering through Phaze’s render happens because of it.
What’s NOT in phaze-astro
Section titled “What’s NOT in phaze-astro”A few things you might expect to find here but don’t:
- HMR injection — that’s phaze-vite’s
phazeVite()plugin. The Astro integration doesn’t add it automatically because HMR is an opt-in dev-only feature with a one-line config cost; app authors who want it addphazeVite()tovite.pluginsthemselves. phazeChunks()— also from phaze-vite. The integration doesn’t set amanualChunksdefault because chunk layout is an app-level decision (how much do you care about cache granularity vs first-paint bytes?).- JSX type augmentations for the
use:/class:/bind:/for:namespaces — those live in@madenowhere/phaze’stypes/jsx.ts(the runtime’s own types). The integration doesn’t ship them because they’re needed by.tsxfiles whether or not Astro is in the picture.
The split is intentional: phaze-astro is only the Astro-specific glue. Everything that could conceivably apply to a non-Astro Vite app lives in phaze-vite or phaze-compile.
The Actions bridge — useAction
Section titled “The Actions bridge — useAction”The other thing phaze-astro ships is @madenowhere/phaze-astro/actions, a small helper that adapts Astro’s typed Actions API to Phaze signals:
import { useAction } from '@madenowhere/phaze-astro/actions'import { actions } from 'astro:actions'
const submit = useAction(actions.requestWaitlistCode)
// submit.pending() → reactive boolean// submit.error() → reactive error or null// submit.data() → reactive last response// submit.execute({ email, turnstileToken }) → fire the actionuseAction wraps Astro’s action call in a Phaze signal.async-flavored object. Same pending/value/error API as signal.async, just keyed off an Astro action call instead of a fetch loader. The wrapper handles abort-on-rerun cleanup via abortSignal() the same way signal.async does.
This is an Astro-Actions-specific helper, so it lives here rather than in phaze core or phaze-compile. Other Astro adapters (Cloudflare’s @astrojs/cloudflare) compose with it transparently — the Actions API and useAction work whether the deployment target is Cloudflare Workers, Node, or static prerendering.
Where to go from here
Section titled “Where to go from here”- Astro reference page — the canonical install + setup guide.
- SSR & hydration — what happens during SSR, how
<astro-island>hydrates, theclient:onlyvsclient:loadtrade-off. - phaze-astro source on GitHub — small package (~200 lines including the actions wrapper), worth a read to see the full integration in one sitting.