Hot reload

sluurp serve watches the app folder. Save a file and every open page updates in place, keeping its state: counters keep their count, dialogs stay open, inputs keep their text. No setup, no bundler.

sluurp serve --public ./app

Swapped in place

The saved module replaces the old one and the page keeps its state. Each case below is covered by the hot lab, a test app that makes every kind of edit and checks state survives:

  • Components: local state, module-level signals, open dialogs, keyed lists, child components in other files.
  • Plain modules: .ts files imported by components, nested imports, circular imports.
  • Data and assets: .json, import.meta.glob (including added/deleted files), ?raw and ?url imports, import.meta.env from .env.
  • Styles: stylesheets and CSS modules, swapped without re-rendering.
  • Workers: TypeScript workers, restarted with the new code.
islands/counter.tsx
import { signal } from "sluurp/reactive";

export default function Counter() {
  const count = signal(0);
  // Change this label and save: the count stays where it was.
  return <button onClick={() => count.set(count() + 1)}>Clicked {count} times</button>;
}

Re-rendered or reloaded

  • Markdown pages, layouts and server routes are re-rendered and patched into the page, without a reload.
  • Modules with top-level side effects (not just definitions) trigger a full reload, since running them twice would repeat the effect.

When something breaks

Compile errors and uncaught exceptions show in the dev overlay with the source line and a link to open it in your editor. Non-fatal problems (a failed request, a console.error) are counted in a corner badge instead. Fix and save; the overlay clears itself.

Pre-bundling and caching

Your own files are served individually so each can be hot-swapped. Libraries aren’t: at startup Sluurp pre-bundles every library your app imports (built-ins like sluurp/reactive and the UI kit, plus vendored packages), like Vite does.

  • No duplicates. It’s a single build with code splitting, so shared code is loaded once.
  • Only what you use. Only the names your code imports are exported. Imports used only by lazily loaded screens load with those screens.
  • Immutable caching. Bundles and your modules are served at content-hashed URLs (/session.ts?v=3f9a1c07) and cached forever, so a reload only fetches what changed.
  • Automatic lazy loading. A component rendered only conditionally (open() ? <Invoice/> : "", ready && <Chart/>) and never used as a value is fetched the first time it renders, with its exclusive dependencies. You write a normal import; the server rewrites it. Components every page renders stay eager, so nothing pops in late.

Importing something new from a library rebuilds the bundle and reloads the page (about a second). To hot-swap edits to a library itself, serve with SLUURP_PREBUNDLE=off.

Checks on every edit

When a file changes, Sluurp checks it for two mistakes and prints them in the server log and the browser console:

  • a component used in JSX that nothing declares (<Buton> for <Button>);
  • an import nothing uses.
src/app.tsx:12:8 Buton is not defined

There’s nothing to set up and no rules to configure: these checks come from the same parse that compiles the file.

In production

Hot reload is dev-only. With --no-hot-reload or a published bundle, nothing is watched and no reload script is injected.

The hot lab lives in e2e/hot-lab, one card per case; its browser test edits each and checks state is preserved.