# @gyral/ssr

> Gyral server rendering: Declarative Shadow DOM pages with hydration seeds.

Source: https://gyral.dev/docs/api/ssr/

Gyral server rendering: Declarative Shadow DOM pages with hydration seeds.

```sh
npm install @gyral/ssr
```

> **Writing code? Start from working code, not from this page.** [Server rendering](https://gyral.dev/docs/server-rendering/). Adapt the closest example and check it, then look a name up here when you are about to call it.

Generated from the package's type declarations. Entry points: [`@gyral/ssr`](#gyralssr), [`@gyral/ssr/static`](#gyralssrstatic), [`@gyral/ssr/node`](#gyralssrnode).

## `@gyral/ssr`

### Functions

| Name | Summary |
| --- | --- |
| [`contentSecurityPolicy`](#ssr-contentsecuritypolicy) | A `Content-Security-Policy` header value whose `style-src` allows Gyral's `<style>` elements by hash, for the components registered when it is called (import them first). |
| [`formAction`](#ssr-formaction) | A request handler for a `<form method="post">` that works with JavaScript disabled, and answers the JS path (`submitForm` in `@gyral/http`, `Accept: application/json`) with JSON: valid → the handler's response, a redirect turned into `{ _tag: 'Redirected', location }`; invalid → `422` with the `IntentRejected` (without `values`). |
| [`rejectWith`](#ssr-rejectwith) | Rejects a submission that passed the schema but failed a server-only check (email taken, wrong password). |
| [`renderPage`](#ssr-renderpage) | An HTML `Response` for a full page, its body pulled in chunks from a synchronous render (one component boundary per pull): load data first, since nothing is awaited and there is no async or suspense streaming. |
| [`renderToString`](#ssr-rendertostring) | Renders to a complete string (tests, caching, static generation), with this request's stores in scope (ADR 0013). |
| [`seeOther`](#ssr-seeother) | 303 See Other: the redirect that turns a POST into a GET (no resubmit on reload). |
| [`styleHashes`](#ssr-stylehashes) | `'sha256-…'` hashes of every registered shadow component's declarative-shadow-root `<style>`, for a `Content-Security-Policy` `style-src`. |
| [`wantsJson`](#ssr-wantsjson) | Whether the request asks for JSON rather than a page (`Accept: application/json` without `text/html`): `submitForm` and other fetch calls do, a browser's form post doesn't. |

#### `contentSecurityPolicy`

```ts
function contentSecurityPolicy(options?: CspOptions): Promise<string>;
```

A `Content-Security-Policy` header value whose `style-src` allows Gyral's `<style>`
elements by hash, for the components registered when it is called (import them first).
`renderPage({ …, csp: options })` builds it at render time instead; pass this result to
`renderPage({ …, csp })` or set it on any response. Other inline styles (`style`
attributes, hand-written `<style>` in `head`) are not covered.

#### `formAction`

```ts
function formAction<Schema extends StandardSchemaV1>(definition: FormDefinition<Schema>, handlers: FormActionHandlers<StandardSchemaV1.InferOutput<Schema>>): (request: Request) => Promise<Response>;
```

A request handler for a `<form method="post">` that works with JavaScript disabled, and
answers the JS path (`submitForm` in `@gyral/http`, `Accept: application/json`) with JSON:
valid → the handler's response, a redirect turned into `{ _tag: 'Redirected', location }`;
invalid → `422` with the `IntentRejected` (without `values`). On the no-JS path `values`
never includes the definition's secret fields (`defineForm(schema, { secret })`).

#### `rejectWith`

```ts
function rejectWith(issues: string | readonly FieldIssue[]): FormReject;
```

Rejects a submission that passed the schema but failed a server-only check (email taken,
wrong password). A string is a form-level message (path `''`). `formAction` turns it into
the same `IntentRejected` as a schema failure, on both the no-JS and the JS path.

#### `renderPage`

```ts
function renderPage(options: PageOptions, init?: ResponseInit): Response;
```

An HTML `Response` for a full page, its body pulled in chunks from a synchronous render (one
component boundary per pull): load data first, since nothing is awaited
and there is no async or suspense streaming. Status and headers are final before the first
byte. `csp` sets the `Content-Security-Policy`
header: a string as is, or `contentSecurityPolicy()`'s options to build it now, with every
component registered by the time the page renders (and the page's `styles`). With
`csp: { styleAttributes: 'hash' }` the page is rendered to a string first, so the header can
list its `style` attribute values by hash (ADR 0020); a render error then throws here.
A component whose `init` or view throws renders its error view and the page goes on;
`onError` hears it, and `onError: 'throw'` renders to a string first and throws here instead,
before any byte is sent (ADR 0024).

#### `renderToString`

```ts
function renderToString(value: ChildValue, options?: RenderOptions): Promise<string>;
```

Renders to a complete string (tests, caching, static generation), with this request's stores
in scope (ADR 0013).

#### `seeOther`

```ts
function seeOther(location: string): Response;
```

303 See Other: the redirect that turns a POST into a GET (no resubmit on reload).

#### `styleHashes`

```ts
function styleHashes(): Promise<readonly string[]>;
```

`'sha256-…'` hashes of every registered shadow component's declarative-shadow-root `<style>`,
for a `Content-Security-Policy` `style-src`.

#### `wantsJson`

```ts
const wantsJson: (request: Request) => boolean
```

Whether the request asks for JSON rather than a page (`Accept: application/json` without
`text/html`): `submitForm` and other fetch calls do, a browser's form post doesn't. The rule
`formAction` answers by; for your own routes that serve both.

### Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

| Name | Summary |
| --- | --- |
| [`ComponentAssets`](#ssr-componentassets) | The client side of automatic component loading, for `renderPage({ components })`: from `componentsFromManifest()` in production, handed over by `productionServer` and `gyralDevServer`. |
| [`ComponentModule`](#ssr-componentmodule) | One component's client module: what to preload and link when the page renders its tag. |
| [`CspDirectives`](#ssr-cspdirectives) | CSP directives by name: a source list, as one string or a list of sources. |
| [`CspOptions`](#ssr-cspoptions) |  |
| [`FormActionHandlers`](#ssr-formactionhandlers) |  |
| [`FormReject`](#ssr-formreject) | A server-side rejection of a schema-valid submission (see `rejectWith`). |
| [`PageOptions`](#ssr-pageoptions) | The page shell's options. |
| [`RenderOptions`](#ssr-renderoptions) |  |

#### `ComponentAssets`

```ts
interface ComponentAssets {
    /** URL of the loader module, which imports each rendered tag's module when it's in the page. */
    readonly loader: string;
    /** Tag → its client module. A rendered tag missing here won't hydrate (development warns). */
    readonly modules: ReadonlyMap<string, ComponentModule>;
    /** The loader's own imports, preloaded with it (production). */
    readonly preload?: readonly string[];
    /** More module scripts for pages with components (development: Vite's client). */
    readonly scripts?: readonly string[];
}
```

The client side of automatic component loading, for `renderPage({ components })`: from
`componentsFromManifest()` in production, handed over by `productionServer` and
`gyralDevServer`.

#### `ComponentModule`

```ts
interface ComponentModule {
    /** The module's chunk and its static imports (production; empty in development). */
    readonly preload: readonly string[];
    /** Hashed CSS the module imports (production; empty in development). */
    readonly stylesheets: readonly string[];
}
```

One component's client module: what to preload and link when the page renders its tag.

#### `CspDirectives`

```ts
type CspDirectives = Readonly<Record<string, string | readonly string[]>>;
```

CSP directives by name: a source list, as one string or a list of sources.

#### `CspOptions`

```ts
interface CspOptions {
    /**
     * Precomputed component `<style>` hashes from `styleHashes()` at build time. Append them
     * to registered component hashes, allowing a policy-only server to read JSON metadata
     * without executing every component module. Regenerate with each release's actual CSS.
     */
    readonly componentStyleHashes?: readonly string[];
    /**
     * Your other directives (`default-src`, `script-src`, …). A `style-src` given here is kept
     * and the hashes are appended; without one it is `'self'` plus the hashes.
     */
    readonly directives?: CspDirectives;
    /**
     * The page's global styles, exactly as passed to `renderPage({ styles })`, hashed too. In
     * `renderPage({ csp })` it defaults to the page's own `styles`.
     */
    readonly styles?: string | readonly string[];
    /**
     * The page's inline head scripts, exactly as passed to `renderPage({ headScripts })`, allowed in
     * `script-src` by hash (a `script-src` given in `directives` is kept and the hashes appended;
     * without one it is `'self'` plus the hashes). In `renderPage({ csp })` it defaults to the
     * page's own `headScripts`.
     */
    readonly scripts?: string | readonly string[];
    /**
     * `'hash'` (`renderPage` only, ADR 0020): allow the `style` attributes the page writes by
     * hash, in `style-src-attr` with `'unsafe-hashes'`, so they apply on first paint under a
     * strict policy. The page is rendered to a string before the response is built (its
     * headers need every value), so the body isn't chunked. Covers static, bound, multi-part and
     * element-hook values, not `raw()` markup. A `style-src-attr` given in `directives` is kept;
     * when it allows `'unsafe-inline'`, no hashes are added (a hash would disable it).
     */
    readonly styleAttributes?: 'hash';
    /**
     * The most `style` attribute hashes one page's header lists (default 128, about 7 KB of
     * header; proxies reject large headers). Values past it are left out and logged once;
     * hydration applies them.
     */
    readonly maxStyleHashes?: number;
}
```


#### `FormActionHandlers`

```ts
interface FormActionHandlers<T> {
    /**
     * The form's `data-intent` name. It becomes `IntentRejected.intent`, exactly as on the
     * JS path, so the component's reducer can't tell the two apart.
     */
    readonly intent: string;
    /**
     * Valid submission: do the work, then usually `seeOther(url)` (Post/Redirect/Get), or return
     * `rejectWith(…)` when a server-only check fails.
     */
    readonly valid: (data: T, request: Request) => Response | FormReject | Promise<Response | FormReject>;
    /**
     * Invalid submission without JavaScript: re-render the page with the component's
     * `.initialMessages` set to `[rejected]`, typically with status 422. The component's own
     * `IntentRejected` reducer turns it into state, so the errors render through the same view as
     * with JavaScript. Leave it out for an endpoint only `submitForm` calls: every rejection is
     * then the JSON 422.
     */
    readonly invalid?: (rejected: IntentRejected, request: Request) => Response | Promise<Response>;
}
```


#### `FormReject`

```ts
interface FormReject {
    readonly _tag: 'FormReject';
    readonly issues: readonly FieldIssue[];
}
```

A server-side rejection of a schema-valid submission (see `rejectWith`).

#### `PageOptions`

```ts
interface PageOptions extends RenderOptions, Head {
    /** The hydratable app: an `html` template, usually one custom element. */
    readonly body: ChildValue;
    /**
     * Server-only head content the head model doesn't cover, written with `html` (icons that
     * never change, preconnect hints, …). Never touched by `setHead()`.
     */
    readonly extraHead?: ChildValue;
    /**
     * Global CSS for the document (your app's own stylesheet text, e.g. a `?raw` import), written
     * as `<style>` elements in the head. A `</style` inside the text is escaped, so it can't
     * close the element early. Trusted CSS only: never put user input here.
     */
    readonly styles?: string | readonly string[];
    /**
     * Stylesheet URLs, written as `<link rel="stylesheet">` before `styles`: in production,
     * `clientAssetsFromManifest()`'s `css` (the hashed CSS Vite emitted for what the client entry
     * imports), served immutable and allowed by `style-src 'self'` without hashes.
     */
    readonly stylesheets?: readonly string[];
    /** Module scripts to load, e.g. the client entry. */
    readonly scripts?: readonly string[];
    /**
     * Inline classic scripts written early in the head, after the managed head entries and
     * before any stylesheet, so they run before the page paints (a theme script that sets
     * `data-theme` from storage, say). Trusted code only. A `</script` inside the text is escaped.
     * `renderPage({ csp })` allows them in `script-src` by hash.
     */
    readonly headScripts?: string | readonly string[];
    /**
     * Modules to fetch early with `<link rel="modulepreload">`, written before `scripts`: in
     * production, `clientAssetsFromManifest()`'s `modulepreload` (the entry's static imports and
     * the hydration chunk), so hydration doesn't wait for extra round trips.
     */
    readonly modulepreload?: readonly string[];
    /**
     * `renderPage` only: the `Content-Security-Policy` header. Pass `contentSecurityPolicy()`'s
     * options (`{ directives }`; the page's `styles` are included) to build it when the page
     * renders, with every component registered by then and the `<style>` elements allowed by
     * hash; or a header value, e.g. from `await contentSecurityPolicy({ styles })` (development
     * warns when it lacks a registered component's hash).
     */
    readonly csp?: string | CspOptions;
}
```

The page shell's options. The `Head` fields (title, description, canonical, robots, meta,
links, JSON-LD, lang, dir) are the managed head (ADR 0019): the same object a client router
applies with `setHead()` after a navigation, so build both from one function.

#### `RenderOptions`

```ts
interface RenderOptions {
    /**
     * This request's store instances (ADR 0013). Components read them during the render; a
     * store not listed starts from its `init`. Create new instances per request.
     */
    readonly stores?: readonly AnyStoreInstance[];
    /**
     * Development markers and checks. Default: on when
     * `@gyral/core` resolves with the `development` condition (Vite's dev server, Vitest).
     */
    readonly dev?: boolean;
    /**
     * A component whose `init` or view throws (ADR 0024): by default the page goes on with that
     * component's error view (or an empty host the browser starts fresh) and the `GyralError` is
     * logged with `console.error`. A function hears each failure instead (logging, monitoring).
     * `'throw'` ends the render with the failure instead; `renderPage` then renders the whole page
     * first, so it throws before any byte is sent and the route can answer with an error page.
     */
    readonly onError?: ((error: GyralError) => void) | 'throw';
    /**
     * Automatic component loading: the page links the client modules of exactly the components
     * it rendered (their preload links and the loader), after the body, and nothing when it
     * rendered none. `productionServer` and `gyralDevServer` hand it to `createApp`.
     */
    readonly components?: ComponentAssets;
}
```


## `@gyral/ssr/static`

### Functions

| Name | Summary |
| --- | --- |
| [`assetHandler`](#ssr-static-assethandler) | Serves content-hashed files from `dir` at `prefix` (`/assets/` by default) for GET and HEAD, with `cache-control: public, max-age=31536000, immutable`, `content-type` by extension, `content-length` and `x-content-type-options: nosniff`. |
| [`clientAssetsFromManifest`](#ssr-static-clientassetsfrommanifest) | `ClientAssets` for `entry`, read from a Vite build manifest (`build.manifest: true`). |
| [`componentsFromManifest`](#ssr-static-componentsfrommanifest) | `componentAssets` read from a client build directory (`dist/client`): its `.vite/manifest.json` and the preset's `.vite/gyral-components.json`. |
| [`prerender`](#ssr-static-prerender) | Renders each path through the app (exactly what a request would get) and writes the HTML. |
| [`productionServer`](#ssr-static-productionserver) | A production request handler (framework-neutral: mount its `fetch` in Hono, Node, …): - `GET`/`HEAD /assets/*`: content-hashed client build output, cached immutable (`assetHandler`); - `GET`/`HEAD` of a prerendered path: the `static/` file, revalidated on every use; - everything else (other GETs, POSTs): the request-time app, `no-cache` unless it set one. |
| [`writeComponentStyleManifest`](#ssr-static-writecomponentstylemanifest) | After the client build, automatically import its discovered/listed component modules in a temporary SSR bundle and fresh Node process, then write their real `styleHashes()` as JSON. |

#### `assetHandler`

```ts
function assetHandler(options: AssetHandlerOptions): AssetHandler;
```

Serves content-hashed files from `dir` at `prefix` (`/assets/` by default) for GET and HEAD,
with `cache-control: public, max-age=31536000, immutable`, `content-type` by extension,
`content-length` and `x-content-type-options: nosniff`. Paths that are not under `dir` after
decoding (`..`, encoded slashes leading out) or have a dot segment (`.hidden`) are refused;
a malformed escape is a 400. Misses are 404 with `cache-control: no-store`, so a CDN never
keeps a miss for a file the next deploy brings. Other methods get 405. A single-range
`Range` request gets a 206 with that slice (media seeking: Safari won't play video without
it); several ranges get the whole file. Hashed files never change, so `If-Range` isn't needed.

#### `clientAssetsFromManifest`

```ts
function clientAssetsFromManifest(manifestPath: string, entry: string, also?: readonly string[], options?: ManifestOptions): Promise<ClientAssets>;
```

`ClientAssets` for `entry`, read from a Vite build manifest (`build.manifest: true`). `also`:
manifest keys of modules the page will import lazily (a route's module, by source path such
as `src/routes/product.ts`), preloaded too, each with its static imports and CSS.
`options.base`: Vite's `base`, which every URL starts with.

#### `componentsFromManifest`

```ts
function componentsFromManifest(clientDir: string, options?: ManifestOptions): Promise<ComponentAssets | undefined>;
```

`componentAssets` read from a client build directory (`dist/client`): its
`.vite/manifest.json` and the preset's `.vite/gyral-components.json`. Undefined when the
build wasn't made with `components: true`.

#### `prerender`

```ts
function prerender(options: PrerenderOptions): Promise<readonly PrerenderedPage[]>;
```

Renders each path through the app (exactly what a request would get) and writes the HTML.
A non-200 response fails the build: an `ssg` route must render successfully.

#### `productionServer`

```ts
function productionServer<Env = unknown>(options: ProductionOptions<Env>): Promise<FetchApp<Env>>;
```

A production request handler (framework-neutral: mount its `fetch` in Hono, Node, …):
- `GET`/`HEAD /assets/*`: content-hashed client build output, cached immutable
  (`assetHandler`);
- `GET`/`HEAD` of a prerendered path: the `static/` file, revalidated on every use;
- everything else (other GETs, POSTs): the request-time app, `no-cache` unless it set one.

Its `fetch` passes the server's `env` (`toNodeListener`'s `{ incoming, remoteAddress }`) on to
the app, so the app can read the client's address.

#### `writeComponentStyleManifest`

```ts
function writeComponentStyleManifest(options: ComponentStyleManifestOptions): Promise<ComponentStyleManifest>;
```

After the client build, automatically import its discovered/listed component modules in a
temporary SSR bundle and fresh Node process, then write their real `styleHashes()` as JSON.
This executes trusted application module initialization at BUILD time: modules must be
SSR-safe. It is not AST guessing and must not run against untrusted source. Vite is loaded
only when invoked; production reads the resulting JSON into `CspOptions.componentStyleHashes`.

### Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

| Name | Summary |
| --- | --- |
| [`AppAssets`](#ssr-static-appassets) | What `productionServer` hands `createApp`: the built entry and what pages link with it. |
| [`AssetHandler`](#ssr-static-assethandler) | Answers a request under the prefix, `undefined` for any other path. |
| [`AssetHandlerOptions`](#ssr-static-assethandleroptions) |  |
| [`ClientAssets`](#ssr-static-clientassets) | What a server-rendered page loads: the client entry, the modules to preload, the CSS. |
| [`ComponentStyleManifest`](#ssr-static-componentstylemanifest) |  |
| [`ComponentStyleManifestOptions`](#ssr-static-componentstylemanifestoptions) |  |
| [`FetchApp`](#ssr-static-fetchapp) | Anything with a fetch handler: a Hono app, or a plain function. |
| [`ManifestChunk`](#ssr-static-manifestchunk) | One chunk of Vite's `.vite/manifest.json` (the fields serving uses). |
| [`ManifestOptions`](#ssr-static-manifestoptions) | Where the build is served: Vite's `base` (default `/`), which the manifest's paths omit. |
| [`PageAssets`](#ssr-static-pageassets) | The page-level parts of `ClientAssets`, named as `renderPage` takes them. |
| [`PrerenderedPage`](#ssr-static-prerenderedpage) |  |
| [`PrerenderOptions`](#ssr-static-prerenderoptions) |  |
| [`ProductionOptions`](#ssr-static-productionoptions) |  |
| [`RenderMode`](#ssr-static-rendermode) | How a route is rendered. |
| [`ViteManifest`](#ssr-static-vitemanifest) | Vite's build manifest (`build.manifest: true`): chunks by source path or chunk key. |

#### `AppAssets`

```ts
interface AppAssets extends PageAssets {
    /** The built client entry's URL; undefined when the build has none (components only). */
    readonly clientEntry: string | undefined;
    /**
     * For `renderPage({ components })` when the build was made with
     * `gyralVitePreset({ components: true })`: each page links exactly the components it rendered.
     */
    readonly components: ComponentAssets | undefined;
    /** `modulepreload` and `stylesheets` with `modules` added (cached per list). */
    readonly assets: (modules: readonly string[]) => PageAssets;
    /** `assets(modules).modulepreload`. */
    readonly preload: (modules: readonly string[]) => readonly string[];
}
```

What `productionServer` hands `createApp`: the built entry and what pages link with it.

#### `AssetHandler`

```ts
type AssetHandler = (request: Request) => Promise<Response | undefined>;
```

Answers a request under the prefix, `undefined` for any other path. Never throws.

#### `AssetHandlerOptions`

```ts
interface AssetHandlerOptions {
    /** The directory served under `prefix`, e.g. `dist/client/assets`. */
    readonly dir: string;
    /** The URL path prefix, with both slashes. Default `/assets/`. */
    readonly prefix?: string;
    /**
     * Keep served files in memory (hashed files never change): `true` (default) up to 64 MiB,
     * `{ maxBytes }` for another bound (least recently served files are dropped first), `false`
     * to read every request from disk. Misses are never cached.
     */
    readonly cache?: boolean | {
        readonly maxBytes: number;
    };
}
```


#### `ClientAssets`

```ts
interface ClientAssets {
    /** The entry chunk's URL, for `renderPage({ scripts })`. */
    readonly entry: string;
    /**
     * Chunks the entry is known to need, for `renderPage({ modulepreload })`: the entry itself first
     * (when there is anything else), then its static imports and Gyral's lazily loaded hydration
     * chunk with its imports. Without the hints the browser
     * finds them only after it has fetched and parsed the entry (a server-rendered page would
     * fetch the hydration chunk a round trip later still).
     */
    readonly modulepreload: readonly string[];
    /**
     * The hashed CSS files of those chunks, for `renderPage({ stylesheets })`: what Vite emitted for
     * CSS imported by the entry, its static imports and the `also` modules, each chunk's after
     * its imports' (module order), each file once.
     */
    readonly css: readonly string[];
}
```

What a server-rendered page loads: the client entry, the modules to preload, the CSS.

#### `ComponentStyleManifest`

```ts
interface ComponentStyleManifest {
    readonly schema: 1;
    /** Sorted, deduplicated, quoted CSP SHA-256 sources for actual serialized shadow styles. */
    readonly hashes: readonly string[];
}
```


#### `ComponentStyleManifestOptions`

```ts
interface ComponentStyleManifestOptions {
    /** Application root used by its client build. Its component modules must be SSR-safe. */
    readonly root: string;
    /** Client output from `gyralVitePreset({ components: true })`, including `.vite` metadata. */
    readonly clientDir: string;
    /** Standalone JSON deployment artifact; no component module is needed to read it later. */
    readonly outFile: string;
    /** Build-time aliases/plugins used by application modules. No config file is loaded. */
    readonly vite?: InlineConfig;
}
```


#### `FetchApp`

```ts
interface FetchApp<Env = unknown> {
    readonly fetch: (request: Request, env?: Env) => Response | Promise<Response>;
}
```

Anything with a fetch handler: a Hono app, or a plain function. `env` is what the server
passes along (`toNodeListener`'s `{ incoming, remoteAddress }`); a prerender passes none.

#### `ManifestChunk`

```ts
interface ManifestChunk {
    readonly file: string;
    readonly imports?: readonly string[];
    readonly dynamicImports?: readonly string[];
    readonly css?: readonly string[];
}
```

One chunk of Vite's `.vite/manifest.json` (the fields serving uses).

#### `ManifestOptions`

```ts
interface ManifestOptions {
    /** Vite's `base`, e.g. `/app/` (a missing trailing slash is added). Default `/`. */
    readonly base?: string;
}
```

Where the build is served: Vite's `base` (default `/`), which the manifest's paths omit.

#### `PageAssets`

```ts
interface PageAssets {
    readonly modulepreload: readonly string[];
    readonly stylesheets: readonly string[];
}
```

The page-level parts of `ClientAssets`, named as `renderPage` takes them.

#### `PrerenderedPage`

```ts
interface PrerenderedPage {
    readonly path: string;
    readonly file: string;
    readonly status: number;
}
```


#### `PrerenderOptions`

```ts
interface PrerenderOptions {
    readonly app: FetchApp;
    /** URL paths to render, e.g. every `ssg` route of the route table. */
    readonly paths: readonly string[];
    /** Directory the HTML files go to (`/about` → `<outDir>/about/index.html`). */
    readonly outDir: string;
    /** Origin used for the requests (absolute URLs in canonical links etc.). */
    readonly origin?: string;
}
```


#### `ProductionOptions`

```ts
interface ProductionOptions<Env = unknown> {
    /** Build output: `client/` (Vite, with `.vite/manifest.json`) and `static/` (prerendered). */
    readonly distDir: string;
    /**
     * The client entry as named in the Vite manifest. Default `src/entry-client.ts`. A build made
     * with `gyralVitePreset({ components: true })` may have none: pages then load components
     * through `components` alone.
     */
    readonly entry?: string;
    /**
     * The directory served at `/assets/`. Default `<distDir>/client/assets`; point it at a
     * directory that keeps older releases' hashed files too, so tabs opened before a deploy keep
     * loading their chunks.
     */
    readonly assetsDir?: string;
    /**
     * Prerendered pages. Default `<distDir>/static`; `false` when the app prerenders nothing, so
     * page requests don't look for files first.
     */
    readonly staticDir?: string | false;
    /**
     * Vite's `base` when the build is served under a path (e.g. `/app/`): the entry, preload and
     * stylesheet URLs start with it, and assets are served at `<base>assets/`. Default `/`.
     */
    readonly base?: string;
    /** Keep served assets in memory (`assetHandler`'s `cache`). Default `true`. */
    readonly cache?: boolean | {
        readonly maxBytes: number;
    };
    /**
     * Builds the request-time renderer (the same app the prerender step used). Pass
     * `modulepreload` and `stylesheets` on to `renderPage` so pages preload what the entry needs
     * and link its hashed CSS. A page that imports modules lazily (a route's chunk) spreads
     * `assets(modules)` instead: both lists plus those modules, their imports and their CSS
     * (`clientAssetsFromManifest`' `also`). `preload(modules)` is `assets(modules).modulepreload`.
     */
    readonly createApp: (options: AppAssets) => FetchApp<Env>;
    /**
     * Sees every response before it goes out (built assets, prerendered pages and the app's), to
     * add headers such as a CSP or security headers, or cookies. Return the response to send.
     */
    readonly onResponse?: (response: Response, request: Request) => Response | Promise<Response>;
}
```


#### `RenderMode`

```ts
type RenderMode = 'ssg' | 'ssr' | 'csr';
```

How a route is rendered.

#### `ViteManifest`

```ts
type ViteManifest = Readonly<Record<string, ManifestChunk>>;
```

Vite's build manifest (`build.manifest: true`): chunks by source path or chunk key.

## `@gyral/ssr/node`

### Functions

| Name | Summary |
| --- | --- |
| [`gyralDevServer`](#ssr-node-gyraldevserver) | Starts the development server of a server-rendered app: |
| [`toNodeListener`](#ssr-node-tonodelistener) | A `node:http` request listener that runs `fetch` for each request: - the `Request` gets the method, URL (on `origin`), headers and, for methods other than `GET`/`HEAD`, the body as a stream; its `signal` aborts when the client disconnects; - the `Response` body is piped with backpressure (a slow client pauses the stream, so a `renderPage` body renders as fast as it is read) and cancelled on disconnect; - `HEAD` sends the headers only; each `set-cookie` stays its own header; - an unparsable request target is a 400; a throwing handler a 500 (`onError` sees it); - the handler's second argument is a {@link NodeEnv}: `{ incoming, remoteAddress }`. |

#### `gyralDevServer`

```ts
function gyralDevServer<M, S = undefined>(options: DevServerOptions<M, S>): Promise<DevServer>;
```

Starts the development server of a server-rendered app:


```ts
await gyralDevServer({
  entry: '/server/app.ts',
  app: (mod: typeof import('./app.js'), { components }) => mod.createApp({ components }),
});
```

Client modules, CSS and HMR come from Vite (middleware mode); any other request renders
through the app, streamed. A render error answers 500 with Vite's error overlay.

#### `toNodeListener`

```ts
function toNodeListener(fetch: FetchHandler, options?: NodeListenerOptions): NodeListener;
```

A `node:http` request listener that runs `fetch` for each request:
- the `Request` gets the method, URL (on `origin`), headers and, for methods other than
  `GET`/`HEAD`, the body as a stream; its `signal` aborts when the client disconnects;
- the `Response` body is piped with backpressure (a slow client pauses the stream, so a
  `renderPage` body renders as fast as it is read) and cancelled on disconnect;
- `HEAD` sends the headers only; each `set-cookie` stays its own header;
- an unparsable request target is a 400; a throwing handler a 500 (`onError` sees it);
- the handler's second argument is a {@link NodeEnv}: `{ incoming, remoteAddress }`.

### Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

| Name | Summary |
| --- | --- |
| [`DevApp`](#ssr-node-devapp) | A request handler: a Hono app, `productionServer`'s result, or `{ fetch }`. |
| [`DevServer`](#ssr-node-devserver) |  |
| [`DevServerContext`](#ssr-node-devservercontext) | What `gyralDevServer` hands the app factory with each freshly loaded module. |
| [`DevServerOptions`](#ssr-node-devserveroptions) |  |
| [`FetchHandler`](#ssr-node-fetchhandler) | A fetch handler: `productionServer(…).fetch`, a Hono app's `fetch`, or a plain function. |
| [`NodeEnv`](#ssr-node-nodeenv) | What `toNodeListener` passes as the handler's second argument: the Node request and the client's address. |
| [`NodeListener`](#ssr-node-nodelistener) | A Node `request` listener: `createServer(toNodeListener(app.fetch))`. |
| [`NodeListenerOptions`](#ssr-node-nodelisteneroptions) |  |

#### `DevApp`

```ts
interface DevApp {
    readonly fetch: (request: Request, env: NodeEnv) => Response | Promise<Response>;
}
```

A request handler: a Hono app, `productionServer`'s result, or `{ fetch }`.

#### `DevServer`

```ts
interface DevServer {
    /** `http://host:port/` the server listens on. */
    readonly url: string;
    readonly server: Server;
    readonly vite: ViteDevServer;
    /** Stops the HTTP server and Vite. */
    readonly close: () => Promise<void>;
}
```


#### `DevServerContext`

```ts
interface DevServerContext<S> {
    /**
     * For `renderPage({ components })` when the Vite config uses `gyralVitePreset({ components })`:
     * the loader and the tags it knows. Undefined otherwise.
     */
    readonly components: ComponentAssets | undefined;
    /** The value `state` created once, kept across module reloads. */
    readonly state: S;
}
```

What `gyralDevServer` hands the app factory with each freshly loaded module.

#### `DevServerOptions`

```ts
interface DevServerOptions<M, S> {
    /** The server module, from Vite's root (e.g. `/server/app.ts`), loaded per request. */
    readonly entry: string;
    /** Builds the request handler from the module just loaded. */
    readonly app: (module: M, context: DevServerContext<S>) => DevApp;
    /**
     * State that must outlive module reloads (an in-memory database, say): created once, from the
     * first module loaded, and passed to every `app` call.
     */
    readonly state?: (module: M) => S;
    /** Default: the `PORT` environment variable, else 5173. `0` picks a free port. */
    readonly port?: number;
    /** Default: Node's (all interfaces). */
    readonly host?: string;
    /** The HMR WebSocket's port, so several dev servers can run side by side. Default Vite's. */
    readonly hmrPort?: number;
    /** `toNodeListener`'s `origin`: the public origin when absolute URLs matter. */
    readonly origin?: string;
    /** More Vite settings (root, configFile, plugins, …), merged under the server settings. */
    readonly vite?: InlineConfig;
}
```


#### `FetchHandler`

```ts
type FetchHandler = (request: Request, env: NodeEnv) => Response | Promise<Response>;
```

A fetch handler: `productionServer(…).fetch`, a Hono app's `fetch`, or a plain function. Under
`toNodeListener` it also receives a {@link NodeEnv}; handlers that don't need it ignore it.

#### `NodeEnv`

```ts
interface NodeEnv {
    /** The Node request, for anything `Request` doesn't carry (socket, TLS details). */
    readonly incoming: IncomingMessage;
    /**
     * The peer's IP address (`req.socket.remoteAddress`), or `undefined` once the socket is gone.
     * Behind a proxy this is the proxy: read `X-Forwarded-For` only when the proxy is known.
     */
    readonly remoteAddress: string | undefined;
}
```

What `toNodeListener` passes as the handler's second argument: the Node request
and the client's address. `{ incoming }` is the shape Hono's Node adapter uses, so
`getConnInfo` from `@hono/node-server/conninfo` works unchanged.

#### `NodeListener`

```ts
type NodeListener = (req: IncomingMessage, res: ServerResponse) => void;
```

A Node `request` listener: `createServer(toNodeListener(app.fetch))`.

#### `NodeListenerOptions`

```ts
interface NodeListenerOptions {
    /**
     * The origin request URLs are built on, e.g. `https://example.com` behind a proxy. Default:
     * `http://` (`https://` on a TLS socket) plus the request's `Host` header, which the client
     * chooses: set it when the app writes absolute URLs (canonical links, redirects).
     */
    readonly origin?: string;
    /**
     * Called when the handler throws or the response body fails, before the 500 (or, once the
     * headers are out, before the connection is cut). Default: `console.error`.
     */
    readonly onError?: (error: unknown, request: Request) => void;
}
```
