SSR framework for Svelte 5 + Bun with islands-based selective hydration
On this page
Development mode
Mochi runs in development mode when NODE_ENV=development, and in production mode otherwise. Set the variable in your dev script and leave it unset everywhere else:
// file: package.json
{
"scripts": {
"dev": "NODE_ENV=development bun src/index.ts",
"start": "bun src/index.ts"
}
}Mochi.serve() needs no flag for this — the environment decides:
// file: src/index.ts
await Mochi.serve({ routes });In development mode, Mochi enables:
- Live reload — a
mochi-live-reloadweb component connects to/__mochi_live_reloadand refreshes the page on file changes. - File watcher — watches
src/andpublic/. An edit invalidates the SSR compile cache and emitsfile:changeonmochiEvents. - Debug bar —
<div id="mochi-dev-toolbar">is injected into every page. - Error overlay — build and runtime errors render on top of the page.
- Bundle stats — a JSON report is served at
${assetPrefix}/client/stats. - Stack traces on
errorevents —errorpayloads includestack. In productionstackisundefined.
Overriding the mode
Pass development to force a mode regardless of the environment:
// file: src/index.ts
await Mochi.serve({
development: true, // ignores NODE_ENV
routes,
});Reach for it sparingly. Everything that runs outside Mochi.serve() still reads NODE_ENV — isDev in module top-level code, and setupTailwind, which you call yourself — so an override makes those disagree with the running server. Mochi warns at boot when it contradicts NODE_ENV=development.
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.
File watcher
The watcher always covers src/ and public/. Extend it with additionalWatchPaths:
// file: src/index.ts
await Mochi.serve({
additionalWatchPaths: ['../content', './docs'],
routes,
});additionalWatchPaths is additive. Paths that do not exist on disk are skipped silently.
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 change, 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. WebSocket connections stay open. The browser reloads to pick up updated serverProps.
Mochi warns once per dev session — via the recompile:module-churn event — after the entry has been re-imported ten times, printed by consoleLogger() as a warn-level HMR line. If you have deliberately accepted the churn (or your resources are all held with pinGlobal), silence just that line with a consoleLogger:line filter — the same mechanism the framework uses to hide its own internal routes:
// file: src/index.ts
await Mochi.serve({
filters: {
'consoleLogger:line': (line, { source }) => (source.name === 'recompile:module-churn' ? null : line),
},
routes,
});Returning null drops the line; returning it unchanged keeps it. This suppresses only the churn warning — every other log line is untouched.
file:change event
Every watcher event re-emits on mochiEvents as file:change. Use it to invalidate your own caches:
// file: src/lib/docs-cache.ts
import { mochiEvents } from 'mochi-framework';
mochiEvents.setHandler('docs:cache-clear', 'file:change', ({ path }) => {
if (path.endsWith('.md')) clearMyMarkdownCache();
});In production the watcher never starts and file:change never emits.
HMR rebuild logger lines
The built-in logger prints a structured line per save:
BUILD— one per page compiled, plus a summary with total file count and duration.BNDL— one perbuildClientBundle()call.HMR— one per rebuild cycle. Note shows the trigger,pages=N, andbundles=N.
A healthy bundles= for a non-CSS save is 1. A higher number means a regression to per-page bundling, which is O(N²) work for N hydratable pages.
Barrel-import warning
Mochi warns when a dependency drags a large module into the build graph that is then almost entirely tree-shaken away:
import { Sun } from '@lucide/svelte'; // ❌ parses the whole ~100 KB re-export index every rebuild
import Sun from '@lucide/svelte/icons/sun'; // ✅ pulls only the one iconBun tree-shakes the barrel’s modules, but it still re-parses the package’s big re-export file on every rebuild, which slows HMR. In dev the warning fires once per package. A production mochi-framework build runs the same check and collapses every offender into one grouped summary line.
Tune or silence it with barrelWarnings on Mochi.serve():
await Mochi.serve({
barrelWarnings: false, // silence entirely
// or keep it on but suppress a package / raise the threshold:
barrelWarnings: { ignore: ['@lucide/svelte'], minBytes: 100 * 1024 },
routes,
});minBytes defaults to 50 KB. For richer logic than a static ignore list, register the barrel:warn filter.