HMR & Dev Server
How Lunx's dev server serves your code without bundling it, and how a saved file reaches the browser with component state preserved.
How HMR works in Lunx
After you save a file, three things happen:
- Detection: the file watcher (Node's native
fs.watch, or lunx's optional Rust watcher) reports the change, debounced by 10 ms - Recompilation: only the changed module is recompiled, by Oxc, or by the framework's compiler for Vue, Svelte and similar files
- Push: the update goes to the browser over the dev server's WebSocket, and React Refresh (or the framework's HMR) swaps the module in place
Measured end to end
Time from saving App.tsx to Chromium showing the change, same app for every tool (benchmarks):
| App | lunx | Vite | Rspack | webpack |
|---|---|---|---|---|
| Small React app | 30 ms | 40 ms | 208 ms | 275 ms |
| 2,000 components | 151 ms | 161 ms | 656 ms | 1113 ms |
Configuring the dev server
typescriptlunx.config.ts
import { defineConfig } from 'lunx-dev' export default defineConfig({ framework: 'react', server: { port: 3000, host: '0.0.0.0', // expose to LAN open: true, // launch browser on start https: true, // auto self-signed cert // Custom response headers headers: { 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp', }, // API proxy to backend proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, ''), }, '/ws': { target: 'ws://localhost:8080', ws: true, // enable WebSocket proxying }, }, }, })
HMR boundary behavior
Lunx propagates module updates up the component tree until it finds a component that can accept the hot update. If no boundary is found, a full page reload is triggered automatically.
typescript
// Explicitly accept hot updates in a module if (import.meta.hot) { import.meta.hot.accept((newModule) => { // Handle the updated module here console.log('Module updated:', newModule) }) // Clean up resources before a module is replaced import.meta.hot.dispose((data) => { data.cleanupFn = myCleanup }) }
Custom HMR events
Plugins can emit custom HMR events to the client, enabling advanced patterns like route-level invalidation or store resets without a full reload.
typescript
// In a Lunx plugin (server side): export default function myPlugin() { return { name: 'my-plugin', handleHotUpdate({ file, server }) { if (file.endsWith('.data.ts')) { server.hot.send({ type: 'custom', event: 'data-store:reset', data: { file } }) return [] // prevent default module update } } } } // In the browser (client side): import.meta.hot?.on('data-store:reset', ({ file }) => { console.log('Data store reset triggered by:', file) store.reset() })
Module federation
Remotes fast-refresh inside the host page too: edit a remote's component and the host shows it with state kept.