@gyral/ssr
Gyral server rendering: Declarative Shadow DOM pages with hydration seeds.
npm install @gyral/ssrWriting code? Start from working code, not from this page. 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, @gyral/ssr/static, @gyral/ssr/node.
@gyral/ssr
Functions
| Name | Summary |
|---|---|
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 |
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 |
Rejects a submission that passed the schema but failed a server-only check (email taken, wrong password). |
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 |
Renders to a complete string (tests, caching, static generation), with this request's stores in scope (ADR 0013). |
seeOther |
303 See Other: the redirect that turns a POST into a GET (no resubmit on reload). |
styleHashes |
'sha256-…' hashes of every registered shadow component's declarative-shadow-root <style>, for a Content-Security-Policy style-src. |
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
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
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
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
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
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
function seeOther(location: string): Response;303 See Other: the redirect that turns a POST into a GET (no resubmit on reload).
styleHashes
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
const wantsJson: (request: Request) => booleanWhether 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 |
The client side of automatic component loading, for renderPage({ components }): from componentsFromManifest() in production, handed over by productionServer and gyralDevServer. |
ComponentModule |
One component's client module: what to preload and link when the page renders its tag. |
CspDirectives |
CSP directives by name: a source list, as one string or a list of sources. |
CspOptions |
|
FormActionHandlers |
|
FormReject |
A server-side rejection of a schema-valid submission (see rejectWith). |
PageOptions |
The page shell's options. |
RenderOptions |
ComponentAssets
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
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
type CspDirectives = Readonly<Record<string, string | readonly string[]>>;CSP directives by name: a source list, as one string or a list of sources.
CspOptions
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
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
interface FormReject {
readonly _tag: 'FormReject';
readonly issues: readonly FieldIssue[];
}A server-side rejection of a schema-valid submission (see rejectWith).
PageOptions
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
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 |
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 |
ClientAssets for entry, read from a Vite build manifest (build.manifest: true). |
componentsFromManifest |
componentAssets read from a client build directory (dist/client): its .vite/manifest.json and the preset's .vite/gyral-components.json. |
prerender |
Renders each path through the app (exactly what a request would get) and writes the HTML. |
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 |
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
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
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
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
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
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/HEADof a prerendered path: thestatic/file, revalidated on every use;- everything else (other GETs, POSTs): the request-time app,
no-cacheunless 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
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 |
What productionServer hands createApp: the built entry and what pages link with it. |
AssetHandler |
Answers a request under the prefix, undefined for any other path. |
AssetHandlerOptions |
|
ClientAssets |
What a server-rendered page loads: the client entry, the modules to preload, the CSS. |
ComponentStyleManifest |
|
ComponentStyleManifestOptions |
|
FetchApp |
Anything with a fetch handler: a Hono app, or a plain function. |
ManifestChunk |
One chunk of Vite's .vite/manifest.json (the fields serving uses). |
ManifestOptions |
Where the build is served: Vite's base (default /), which the manifest's paths omit. |
PageAssets |
The page-level parts of ClientAssets, named as renderPage takes them. |
PrerenderedPage |
|
PrerenderOptions |
|
ProductionOptions |
|
RenderMode |
How a route is rendered. |
ViteManifest |
Vite's build manifest (build.manifest: true): chunks by source path or chunk key. |
AppAssets
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
type AssetHandler = (request: Request) => Promise<Response | undefined>;Answers a request under the prefix, undefined for any other path. Never throws.
AssetHandlerOptions
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
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
interface ComponentStyleManifest {
readonly schema: 1;
/** Sorted, deduplicated, quoted CSP SHA-256 sources for actual serialized shadow styles. */
readonly hashes: readonly string[];
}ComponentStyleManifestOptions
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
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
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
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
interface PageAssets {
readonly modulepreload: readonly string[];
readonly stylesheets: readonly string[];
}The page-level parts of ClientAssets, named as renderPage takes them.
PrerenderedPage
interface PrerenderedPage {
readonly path: string;
readonly file: string;
readonly status: number;
}PrerenderOptions
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
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
type RenderMode = 'ssg' | 'ssr' | 'csr';How a route is rendered.
ViteManifest
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 |
Starts the development server of a server-rendered app: |
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
function gyralDevServer<M, S = undefined>(options: DevServerOptions<M, S>): Promise<DevServer>;Starts the development server of a server-rendered app:
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
function toNodeListener(fetch: FetchHandler, options?: NodeListenerOptions): NodeListener;A node:http request listener that runs fetch for each request:
- the
Requestgets the method, URL (onorigin), headers and, for methods other thanGET/HEAD, the body as a stream; itssignalaborts when the client disconnects; - the
Responsebody is piped with backpressure (a slow client pauses the stream, so arenderPagebody renders as fast as it is read) and cancelled on disconnect; HEADsends the headers only; eachset-cookiestays its own header;- an unparsable request target is a 400; a throwing handler a 500 (
onErrorsees 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 |
A request handler: a Hono app, productionServer's result, or { fetch }. |
DevServer |
|
DevServerContext |
What gyralDevServer hands the app factory with each freshly loaded module. |
DevServerOptions |
|
FetchHandler |
A fetch handler: productionServer(…).fetch, a Hono app's fetch, or a plain function. |
NodeEnv |
What toNodeListener passes as the handler's second argument: the Node request and the client's address. |
NodeListener |
A Node request listener: createServer(toNodeListener(app.fetch)). |
NodeListenerOptions |
DevApp
interface DevApp {
readonly fetch: (request: Request, env: NodeEnv) => Response | Promise<Response>;
}A request handler: a Hono app, productionServer's result, or { fetch }.
DevServer
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
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
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
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
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
type NodeListener = (req: IncomingMessage, res: ServerResponse) => void;A Node request listener: createServer(toNodeListener(app.fetch)).
NodeListenerOptions
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;
}