SSR framework for Svelte 5 + Bun with islands-based selective hydration
On this page
Environment constants
Import build-time constants from the mochi-framework virtual module to branch on render target or dev mode:
import { isServer, isBrowser, isDev } from 'mochi-framework';mochi-framework resolves to one of two virtual modules at compile time — server builds export isServer = true, client bundles export isBrowser = true. The values are literal booleans, so if (isBrowser) { … } blocks dead-code-eliminate out of the opposite bundle.
Do NOT read process.env.NODE_ENV or typeof window to detect environment; instead, import these constants — they survive bundling intact.
isServer
true during server-side rendering, false in the browser.
<!-- file: src/lib/HeavyChart.svelte -->
<script>
import { isServer } from 'mochi-framework';
if (isServer) {
// safe to reach into request-scoped APIs here
}
</script>isBrowser
true in the client bundle, false on the server. Use it to gate browser-only APIs (window, document, IntersectionObserver).
<!-- file: src/lib/Lazy.svelte -->
<script>
import { isBrowser } from 'mochi-framework';
if (isBrowser) {
window.addEventListener('scroll', onScroll);
}
</script>isDev
true when Mochi.serve() was started with development: true. Identical on server and client builds.
// file: src/lib/log.ts
import { isDev } from 'mochi-framework';
export function trace(msg: string) {
if (isDev) console.log('[trace]', msg);
}Auto-injected island props
The preprocessor injects one extra prop on every component invoked with mochi:hydrate, mochi:hydrate:visible, or mochi:defer mochi:hydrate:
isHydratable(true | undefined):truefor hydratable invocations, absent on plain SSR-only invocations.
Accept it with $props:
<!-- file: src/lib/Counter.svelte -->
<script lang="ts">
let { isHydratable }: { isHydratable?: boolean } = $props();
</script>For a unique per-instance id (e.g. <label for>), use Svelte’s native $props.id() — see Selective hydration.
Branching SSR-only behavior with isHydratable
Use isHydratable to peek request-scoped state only when the client won’t take over rendering — e.g. read the post-submit form snapshot so the SSR HTML reflects the last action result, but skip it when an enhance attachment will populate state client-side.
<!-- file: src/lib/RandomRoll.svelte -->
<script lang="ts">
import { isServer, getRequestContext } from 'mochi-framework';
let { isHydratable }: { isHydratable?: boolean } = $props();
const initial = isHydratable || !isServer ? null : peekForm();
function peekForm() {
const f = getRequestContext().form;
return f && f.ok && typeof f.data.value === 'number' ? f.data.value : null;
}
</script>See the Forms demo for a side-by-side comparison of hydrated and SSR-only render paths.