--- title: 'Prerendered modules' slug: prerender description: 'Name a module *.prerender.ts to run it at build time and inline its exports, so the code that produced them never ships.' --- ## Prerendered modules A module named `*.prerender.ts` (or `.prerender.js`) runs while your app compiles. The bundler replaces it with its exported values, so the module, and everything it imports, is left out of the bundle. This prerenders a module, not a page — Mochi has no SSG mode, and every request still renders fresh. ```ts // src/demos/url/sources.prerender.ts import { loadSources } from '../../components/utils.ts'; import { files } from './files.ts'; export const sources = await loadSources(files); ``` ```svelte ``` What the bundle holds is the finished value, not the code: ```js const __mochi_export_0__ = [{ label: 'App.svelte', html: '
' }]; export { __mochi_export_0__ as sources }; ``` `loadSources` and `files` are gone, and so is anything they imported — which is the point: a syntax highlighter, a markdown parser, or a database client used only to produce the value stops being a runtime dependency. The suffix is the whole convention, like [`.server.ts`](/docs/server-only-imports/). Top-level `await` works; the build awaits it. Import the module with the extension (`./sources.prerender.ts`) from a `.svelte` or `.md` file, or from a `.ts` module one of them imports. Only the component bundler replaces it: imported from the server entry (`routes.ts`, an API handler), it runs as a plain module, and `moduleRef()` throws. ### What a prerendered module may export Anything [devalue](https://github.com/sveltejs/devalue) can serialize: plain data, `Date`, `Map`, `Set`, `RegExp`, `BigInt`, `undefined`, cycles, and repeated references. Functions, class instances, and promises are a compile error naming the export: ``` src/lib/table.prerender.ts exports "rows", which cannot be inlined: Cannot stringify a function at rows[0].format. ``` Each export is serialized on its own, so an object shared between two exports is inlined twice. ### Returning components with `moduleRef()` A component cannot be serialized. `moduleRef()` marks a module to import instead, and the build turns each marker into a real `import`: ```ts // src/lib/docComponents.prerender.ts import { moduleRef } from 'mochi-framework'; import type { Component } from 'svelte'; import { loadDocs } from './docs'; export const docComponents: Record = Object.fromEntries((await loadDocs()).map((d) => [d.slug, moduleRef(`../../docs/${d.filename}`)])); ``` becomes: ```js import __mochi_ref_0__ from '../../docs/10-intro.md'; const __mochi_export_0__ = { intro: __mochi_ref_0__ }; export { __mochi_export_0__ as docComponents }; ``` Specifiers resolve relative to the `.prerender.ts` file. This replaces generating a barrel file into your source tree before every build, and the map stays typed against the real module. A prerendered module cannot `import` a `.svelte`, `.md`, or `.svelte.ts` file itself — it runs outside the bundler, where Svelte files have no loader. The build rejects the import and points at `moduleRef()`. ### In dev The module is evaluated at build time in dev too, and re-evaluated on every rebuild. An edit to the module or to anything it imports rebuilds the pages that use it. A file it only _reads_ — a directory of markdown, say — is outside the import graph: adding or removing a source or data file (`.md`, `.svelte`, `.ts`, `.json`, …) rebuilds every prerendered module, while an edit to one shows up on the next rebuild of a page that imports the module. Each dev evaluation gets fresh instances of the module's own imports, but the running server keeps its own. A memo that must be shared between the two, say a parsed-docs cache the prerendered module fills and a route reads, has to live on `globalThis` — use `pinGlobal(key, create)` from `mochi-framework`. ### What the build reports `mochi-framework build` prints every module it inlined: ``` Prerendered modules ┌ ✦ src/demos/url/sources.prerender.ts └ ✦ src/lib/docComponents.prerender.ts 2 prerendered modules inlined ``` The value is inlined wherever the module is imported. Inside a `mochi:hydrate` island it is therefore sent to the browser, so never export a secret from a prerendered module a hydrated component imports.