lunx.docs
DocsTutorialsHMR & Dev Server

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):

ApplunxViteRspackwebpack
Small React app30 ms40 ms208 ms275 ms
2,000 components151 ms161 ms656 ms1113 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.