🍡 mochi

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-reload web component connects to /__mochi_live_reload and refreshes the page on file changes.
  • File watcher — watches src/ and public/. An edit invalidates the SSR compile cache and emits file:change on mochiEvents.
  • 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 error eventserror payloads include stack. In production stack is undefined.

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_ENVisDev 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 codeMochi.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 per buildClientBundle() call.
  • HMR — one per rebuild cycle. Note shows the trigger, pages=N, and bundles=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 icon

Bun 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.