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 ./appSwapped 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:
.tsfiles imported by components, nested imports, circular imports. - Data and assets:
.json,import.meta.glob(including added/deleted files),?rawand?urlimports,import.meta.envfrom.env. - Styles: stylesheets and CSS modules, swapped without re-rendering.
- Workers: TypeScript workers, restarted with the new code.
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 definedThere’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.