SSR framework for Svelte 5 + Bun with islands-based selective hydration
Development mode
Development mode is controlled by the development option on Mochi.serve(). It defaults to true, so if you do nothing you get the dev experience.
await Mochi.serve({
development: true, // the default
routes,
});When enabled, Mochi turns on:
- Live reload — pages refresh automatically on file changes (SSE-based; browser listens on
/__mochi_live_reload) - File watcher —
chokidarwatches source files; changes invalidate the component compile cache and notify any subscribers - Debug bar — injected into HTML with component and hydration info
- Error overlay — build and runtime errors displayed in-browser
- Bundle stats — available at
/_mochi/client/stats(or${assetPrefix}/client/statsif customized)
The MODE=development convention
You’ll often see the dev entry wired from a MODE env var:
// src/index.ts
await Mochi.serve({
development: process.env.MODE === 'development',
routes,
});// package.json
{
"scripts": {
"dev": "MODE=development bun src/index.ts",
"start": "bun src/index.ts"
}
}MODE is just a user-space convention — the framework does not read it directly. The framework only reads options.development. Using an env var lets one entry file serve both bun run dev (dev mode) and bun run start (production).
What the file watcher covers
By default, chokidar watches:
src/— your application sourcepublic/— static assets
Use additionalWatchPaths to extend this (for example, a separate content directory):
await Mochi.serve({
additionalWatchPaths: ['../content', './docs'],
routes,
});The defaults are always included — additionalWatchPaths is purely additive. Paths that don’t exist on disk are silently skipped.
Reacting to file changes (file:change event)
Every filesystem change detected by the watcher is emitted on the mochiEvents bus as a file:change event. Use this to invalidate your own caches on edit without restarting the server:
import { mochiEvents } from 'mochi-framework';
if (process.env.MODE === 'development') {
mochiEvents.on('file:change', ({ path, type }) => {
if (path.endsWith('.md')) {
// path is absolute; type is 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
clearMyMarkdownCache();
}
});
}The event fires synchronously, before the debounced browser reload, so subscribers can refresh their state in time for the next request.
mochiEvents is also available from .svelte files during SSR. All server-side copies of the emitter (the main runtime and every SSR bundle) share one pinned instance, so a subscription made from a component running server-side sees events emitted by the framework. On the client, mochiEvents is a stub — on/off/setHandler are no-ops and emit logs a warning to the console, since the bus is server-only.
Subscribing from a module that isn’t your server entry
If your subscribing module can end up bundled into an SSR island (either directly via import in a .svelte file, or transitively), prefer mochiEvents.setHandler(name, type, handler) over .on(...). The dev compile cache re-imports each SSR bundle on every .svelte change, so a module-top-level .on(...) registers a fresh handler each time and they pile up. setHandler replaces any prior registration with the same name — so 15 re-imports collapse to 1 live subscriber.
import { mochiEvents } from 'mochi-framework';
mochiEvents.setHandler('docs-cache-clear', 'file:change', ({ path }) => {
if (path.endsWith('.md')) clearMyMarkdownCache();
});Plain .on(...) remains the right choice when the subscribing module runs exactly once (e.g., your server entry) or when you legitimately want multiple handlers fanning out.
In production (development: false) the watcher is never started and no file:change events are emitted.