@madenowhere/phaze-vite
v0.0.5
Published
Vite/Rollup helpers for Phaze: HMR that re-mounts the changed component instead of reloading the page, plus `phazeChunks` for `manualChunks`. Use alongside @madenowhere/phaze-compile (which handles JSX → DOM compilation).
Downloads
393
Readme
@madenowhere/phaze-vite
Vite plugin for Phaze. HMR for components without a full page reload.
When you edit a .tsx file, Vite normally walks up the import chain looking for a module that accepts its own update. Phaze components don't accept updates by default — so any edit triggers a full page reload, losing all in-page state.
This plugin appends an import.meta.hot.accept(…) callback to every user .tsx / .jsx file. When the file changes, the new module's default export replaces the old one in any active mount, in place. No page reload, no scroll position lost, no module-scope state cleared.
Install
pnpm add -D @madenowhere/phaze-vite// vite.config.ts (plain Phaze + Vite)
import { defineConfig } from 'vite'
import phazeVite from '@madenowhere/phaze-vite'
export default defineConfig({
plugins: [phazeVite()],
})// astro.config.mjs (Astro + Phaze)
import { defineConfig } from 'astro/config'
import phaze from '@madenowhere/phaze-astro'
import phazeVite from '@madenowhere/phaze-vite'
export default defineConfig({
integrations: [phaze()],
vite: { plugins: [phazeVite()] },
})How it works
The plugin runs in dev only (apply: 'serve') and enforce: 'post' so it sees source after @madenowhere/phaze-compile's JSX transform. For each .tsx / .jsx file under your source tree (anything outside node_modules), it appends:
if (import.meta.hot) {
const __phaze_url__ = import.meta.url
import.meta.hot.accept((m) => {
const r = globalThis.__PHAZE_HMR_REPLACE__
if (r && m && typeof m.default === 'function') r(__phaze_url__, m.default)
})
}The handler at globalThis.__PHAZE_HMR_REPLACE__ is set by:
@madenowhere/phaze-astro— when you use the Astro renderer, its client.ts installs an astro-island-aware handler. Mounts are tracked by thecomponent-urlattribute on each<astro-island>.@madenowhere/phaze-vite/runtime— for plain Phaze apps that don't use Astro. Import once at app entry; providestrack()/replace()and installs the global handler.
If neither is present, the boilerplate is a silent no-op and Vite falls back to its default behavior (look up the import chain for an accept, otherwise full reload).
Direct (non-Astro) usage
// src/main.ts
import { hydrate } from 'phaze'
import { track } from '@madenowhere/phaze-vite/runtime'
import App from './App.tsx'
const root = document.getElementById('app')!
track(import.meta.url, root, hydrate, App, {})When App.tsx is edited, the plugin's emitted code calls replace(url, NewApp), which re-mounts at the same root with the new component. Component-local signals reset; module-scope signals (defined outside the component) survive because Vite's HMR re-evaluates only the changed module — anything imported from a separate file keeps its old instance until that file changes.
What survives an edit (today, Tier 1)
| state | survives a .tsx edit? |
|---|---|
| Module-scope signals (in a separate state.ts) | yes |
| Module-scope signals in the edited file itself | no — module re-evaluates, fresh signals |
| Component-local signal() defined inside the component body | no — fresh signals on remount |
| Scroll position | yes |
| Other islands on the page (their state, focus, signals) | yes |
| <input> focus inside the edited component | no — DOM re-mounted |
Tier 2 (transferring component-local signal values when only the template changed) is on the roadmap. Tier 1 is already a substantial dev-loop improvement over full-page reloads.
Test
test/runtime.test.ts covers the registry — track(), replace(), multiple mounts of the same URL, dispose semantics, and the globalThis.__PHAZE_HMR_REPLACE__ install.
Trade-off summary
What you get: dev edits update the page in place instead of reloading.
What it costs: ~120 bytes of boilerplate appended per dev-served .tsx file. Stripped from production builds (apply: 'serve').
What you don't get yet: in-component signal-state preservation. The component re-mounts cleanly; everything inside resets.
