SSR framework for Svelte 5 + Bun with islands-based selective hydration
On this page
Development mode
Set the development flag on Mochi.serve() to switch between the dev and production runtime. It defaults to true.
// file: src/index.ts
await Mochi.serve({
development: true, // the default
routes,
});When development is true, Mochi enables:
- Live reload — a
mochi-live-reloadweb component connects to/__mochi_live_reloadand refreshes the page on file changes. - File watcher —
chokidarwatchessrc/andpublic/; edits invalidate the SSR compile cache and emitfile:changeonmochiEvents. - Debug bar —
<div id="mochi-dev-toolbar">is injected into every page and the per-request payload is seeded ontowindow.__mochi_debug. - Error overlay — build and runtime errors render via
buildErrorOverlayon top of the page (dev only). - Error console script — the same errors are also pushed through
console.errorviabuildErrorScript(runs in dev and prod). - Bundle stats — a JSON report is served at
${assetPrefix}/client/stats(default/_mochi/client/stats). - Stack traces on
errorevents —errorpayloads includestack; in productionstackisundefined.
MODE=development convention
Wire the flag from an env var so one entry file serves both bun run dev and bun run start:
// file: src/index.ts
await Mochi.serve({
development: process.env.MODE === 'development',
routes,
});// file: package.json
{
"scripts": {
"dev": "MODE=development bun src/index.ts",
"start": "bun src/index.ts"
}
}MODE is a user-space convention — Mochi reads only options.development. Do NOT read process.env.NODE_ENV to drive the flag; instead, pass development explicitly so test runners and one-off scripts get the same behaviour.
Live reload
Set liveReload: false to keep the debug bar and file watcher but skip the /__mochi_live_reload WebSocket and the mochi-live-reload web component. Defaults to whatever development is set to.
// file: src/index.ts
await Mochi.serve({
development: true,
liveReload: false, // debug bar stays, WS does not
routes,
});File watcher
The watcher always covers src/ and public/. Extend it with additionalWatchPaths:
// file: src/index.ts
await Mochi.serve({
additionalWatchPaths: ['../content', './docs'],
routes,
});Defaults are always included — additionalWatchPaths is additive. Paths that don’t exist on disk are skipped silently.
Component hot-reload
Svelte component changes are picked up automatically via the SSR compile cache — edit a .svelte file and the browser will refresh.
Route handler HMR
Route handler code — Mochi.api handlers, serverProps resolvers, form actions, Mochi.ws handlers, Mochi.sse handlers — is hot-swapped without a restart. The watcher builds your entry (src/index.ts) to discover its transitive dependencies; when any of them changes it rebuilds the entry, re-reads the routes from its Mochi.serve() call, and updates the running server in place.
Adding, removing, and editing route patterns all work without a restart — new routes are registered, removed routes are cleaned up, and the route table is reloaded on the fly. WebSocket connections stay open; the browser is notified to reload so pages pick up updated serverProps.
file:change event
Every watcher event is re-emitted on mochiEvents as file:change. Use it to invalidate your own caches without restarting:
// file: src/lib/docs-cache.ts
import { mochiEvents } from 'mochi-framework';
mochiEvents.setHandler('docs:cache-clear', 'file:change', ({ path }) => {
if (path.endsWith('.md')) clearMyMarkdownCache();
});path is absolute; type is 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'. The event fires synchronously, before the debounced browser reload, so subscribers are caught up by the time the next request arrives.
Do NOT register file:change handlers with .on() from a module that can be re-imported by the SSR compile cache (e.g. anything imported transitively from a .svelte file); instead, use setHandler with a stable name so re-imports don’t pile up listeners. Plain .on() is fine in your server entry, which is loaded once.
In production (development: false) the watcher never starts and file:change never emits. See events for the full payload reference.
mochiEvents from Svelte SSR
The emitter is pinned on globalThis, so a subscription made from a .svelte file running on the server reaches the same instance the framework emits to. On the client, mochiEvents is a stub: on/off/setHandler are no-ops and emit logs a warning — the bus is server-only.
HMR rebuild logger lines
The built-in logger (default for Mochi.serve()) prints a structured line per save so you can see what was rebuilt and how long it took:
BUILD— one per page compiled, plus a summary line with total file count and duration. Note shows hydratable/server-island counts and SSR bundle size.BNDL— one perbuildClientBundle()call. Note shows the entrypoint count and total output bytes.HMR— one per rebuild cycle. Note shows the trigger (file/css/svelte-config),pages=N(recompiled pages), andbundles=N(calls tobuildClientBundle).
Healthy bundles= for a non-CSS save is 1 — the bundle is deferred to a single trailing call after every page has recompiled. A higher number means a regression to per-page bundling, which is O(N²) work for N hydratable pages.
Do NOT silence the rebuild lines by disabling the whole logger; instead, pass logger: { compile: false } to Mochi.serve() to drop only the BUILD / BNDL / HMR lines.