import { type ServerOptions, server } from "@nifrajs/core/server"; import type { Manifest } from "../manifest.js"; import type { RenderAdapter } from "../render-seam.js"; export interface CreateWebAppOptions { readonly adapter: RenderAdapter; readonly manifest: Manifest; /** URL of the built client entry (module script) injected into every page. */ readonly clientEntry: string; /** Default document title for all pages. */ readonly title?: string; /** * Options for the underlying `server()` - `requestTimeoutMs`, `admission`, `gracefulSignals`, and * the rest of {@link ServerOptions}. * * Needed because the `Server` is constructed in here, so a caller has no other way to reach its * constructor: a page app could set no request timeout and no capacity gate, which are exactly the * two knobs a production readiness check looks for. An SSR app wants them MORE than a backend does - * a render is slow and allocation-heavy, so a burst that a JSON API would absorb is the one that * exhausts the pod. * * Applies to the whole app, {@link mounts} and the {@link api} auto-mount included: the capacity gate * sits at the fetch entry, ahead of the request hooks those are dispatched from. */ readonly server?: ServerOptions; /** * Runs against the app BEFORE any page route is registered - the seam for `securityHeaders()`, * `requestId()`, `logger()`, a rate limit, or anything else applied with `use`. * * ```ts * createWebApp({ * …, * use: (app) => { * app.use(securityHeaders()) * app.use(requestId()) * }, * }) * ``` * * A callback rather than an array because `use` is overloaded - `Middleware`, `IdentityPlugin`, * `ContextPlugin` - and only the real call site instantiates the right overload. The return value * is ignored: this is for cross-cutting concerns, not for declaring routes (declare those on the * returned app, where they get their types). * * **Why the timing matters.** `beforeHandle`, `afterHandle`, `around`, `derive`/`decorate`, and * `onError` are snapshotted into each route AS IT IS DECLARED, and `createWebApp` declares every * page (plus the `/*` catch-all) before it returns - so a caller's `app.use(…)` afterwards binds * them to nothing, silently. `requestId()` is exactly that case: it is a `derive`, so applied late * it leaves every page without `c.requestId`. Route assurance evidence is order-scoped the same * way, so a late `securityHeaders()` leaves the pages unable to PROVE the header to `nifra assure` * even though it does still set it. * * `onRequest`/`onResponse` (and the response header/body hooks) are app-global arrays read at * request time, so those alone DO work when added late. Using this seam for everything avoids * having to remember which is which. * * Runs ahead of the {@link mounts} / {@link api} request hooks, so an `onRequest` middleware guards * a mounted auth handler too. * * A `ContextPlugin` applied here takes runtime effect but cannot widen the declared return type of * this function, so `c.requestId` is not typed on routes you declare afterwards. Named plugins are * idempotent, so `app.use(requestId())` on the returned app recovers the TYPE without applying the * plugin a second time. */ readonly use?: (app: ReturnType>) => void; /** Injected into each loader's `ctx.api` - typically an `inProcessClient(app)` (typed * per-route via `@nifrajs/client`'s `createRoutes`). Opaque to the core. * * **Auto-mount.** Every `inProcessClient(backend)` exposes the symbol-keyed platform-aware backend * mount interface from `@nifrajs/core/mount`; `createWebApp` also serves that backend over HTTP at * {@link apiPrefix} (default `/api`): a request whose pathname starts with the prefix is dispatched * before page routing with the same `env`/`waitUntil` platform context, and the backend's `Response` * is returned untouched. The mount runs in `nifra dev` too. Pass `apiPrefix: ""` to disable it. */ readonly api?: unknown; /** HTTP path prefix the {@link api} backend is auto-mounted at (default `"/api"`). A request whose * pathname is exactly the prefix or starts with `prefix + "/"` is dispatched to the backend before * page routing; the backend therefore defines its routes at the **full** path (`server().post("/api/ * sync", …)`), matching the in-process `inProcessClient` call sites. Set to `""` to disable the * auto-mount entirely (the app serves pages only and `api` stays a loader-only `ctx.api`). Mounting * is also a no-op when `api` does not expose the symbol mount. */ readonly apiPrefix?: string; /** * Strip {@link apiPrefix} from the pathname before dispatching to `api` (default `false`). * * The default suits a backend that declares FULL paths (`server().post("/api/sync", …)`), which is * right when it is only ever mounted here. Set this when the backend declares its routes WITHOUT the * prefix because it also runs standalone behind its own shell, so its paths cannot carry a prefix that * only exists when it is mounted. Without it every request 404s inside the backend, and the workaround * is a `Proxy` that rewrites each URL. */ readonly apiStrip?: boolean; /** * Sub-apps mounted ahead of page routing - an auth handler, a webhook receiver, a stack's routes. * * Structural on purpose: anything with `{ path, app: { fetch } }` fits, so a library that exposes its * routes as such a list mounts as `mounts: theirRoutes` without `@nifrajs/web` taking a dependency on * it. `better-auth` is the motivating case - it is not a `backend` route, so `/api/auth/*` used to 404 * silently. * * Tried longest-path-first and BEFORE the `api` mount, so a mount at `/api/auth` wins over a backend * at `/api` no matter which was declared first. `stripPrefix` is the per-mount form of {@link apiStrip}: * leave it off to pass the full path through. */ readonly mounts?: ReadonlyArray<{ readonly path: string; readonly app: { fetch(request: Request): Response | Promise; }; readonly stripPrefix?: boolean; }>; /** Secret for **draft / preview mode** (see `enableDraft`). When set, a request carrying a valid * signed `__nifra_draft` cookie gets `ctx.draft === true` in loaders/actions (else always `false`). * Pair with `withISR({ draftSecret })` so editors bypass the cache. Omit to disable draft mode. */ readonly draftSecret?: string; /** Per-route chunk URLs (`buildClient`'s `BuildManifest.routes`) - `routeId → [layout chunks…, own * chunk]`. When present, each page `modulepreload`s its matched route's chunks alongside the entry, * so the route code downloads in parallel (no entry→route-chunk waterfall). Omit ⇒ entry-only. */ readonly routePreload?: Readonly>; /** The app's bundled stylesheet URLs (`buildClient`'s `BuildManifest.css`) - the aggregate, injected * as `` in a page's ``. Used as the fallback for any route absent from * {@link routeStyles}. Omit ⇒ no links (dev, where Vite injects CSS, or a CSS-free app). */ readonly styles?: readonly string[]; /** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) - `routeId → [chain CSS]`. * When a matched route has an entry here, only those (its layout chain + own CSS) are linked instead * of the aggregate `styles`, so a page ships only the CSS it uses. An empty array ⇒ no `` (the * page imports no CSS). Routes absent here fall back to `styles`. Omit ⇒ always use `styles`. */ readonly routeStyles?: Readonly>; /** SSG: the prerendered-path set (e.g. `enumerateStaticRoutes(routes).paths` or the build's * `prerendered.json`). Injected as `window.__NIFRA_PRERENDERED__` on every page so a client soft-nav * into a prerendered route fetches its static `_data.json` instead of hitting the worker. */ readonly prerenderedPaths?: readonly string[]; /** Publish the project's `AGENTS.md` inside `/llms.txt` and `/llms-full.txt`. **Off by default**: * those endpoints are public and unauthenticated, while `AGENTS.md` is a repo file written for the * team - unreleased feature names, internal hostnames, and "don't touch X yet" notes live there * routinely. Turn it on only for a repo whose guidelines you would publish as a page. */ readonly publishLocalGuidelines?: boolean; /** SSG: per dynamic route pattern, its `getStaticPaths` `fallback` (from `enumerateStaticRoutes` or * the build's `prerendered.json`). A route mapped to `"404"` rejects any path NOT in * `prerenderedPaths` with the 404 page - the unlisted path simply doesn't exist. `"ssr"` (the * default for unmapped routes) renders unlisted paths on-demand. */ readonly staticFallbacks?: Readonly>; /** In-memory reference cache for explicitly `static` boundary values. It never persists payloads * across processes; operated/durable cache implementations stay outside the public framework. */ readonly staticBoundaryCache?: import("../boundary.js").StaticBoundaryCache; /** Observe every loader/action failure - for error-reporting plugins (Sentry-style). Called for * real throws (not control-flow `Response`s like `redirect`), **before** the nearest `_error` * boundary renders / a soft-nav 500 / a rethrow - so it sees errors that the boundary would * otherwise hide. Observation only; its own throws are swallowed so a faulty reporter can't break * rendering. (`beforeLoader` is intentionally omitted - the core HTTP hooks already cover * pre-request work.) */ readonly onLoaderError?: (error: unknown, ctx: { readonly request: Request; readonly params: Readonly>; readonly route: string; }) => void; } /** * Build a nifra app from a route manifest: every route SSRs its layout chain via `renderPage`, * and a wildcard catch-all renders `_404` (or a plain 404). Reuses @nifrajs/core's router + * lifecycle, so matching, params, and precedence are battle-tested. fs-free - feed it a * manifest from `discoverRoutes` (`@nifrajs/web/fs`) at startup, so the served app stays portable. * * **Typed platform bindings.** Pass `Env` - `createWebApp({ … })` - to declare the app's Workers * bindings ONCE. It seeds the returned `Server`'s context with `{ env: Env }` (exactly as the backend's * `server()` does), so `app.fetch(req, { env })` / `toFetchHandler(app)` type-check the `env` * argument against the declared shape - no per-binding cast at the edge entry. Per-route loaders/actions * stay typed independently of this call: annotate them with `@nifrajs/client`'s `LoaderArgs` * (same `Env`) so `ctx.env.MY_KV` is typed there too. Omit the parameter and `Env` is `unknown` - the * secure default; validate at the trust boundary before use. */ export declare function createWebApp(options: CreateWebAppOptions): ReturnType>; //# sourceMappingURL=web-app.d.ts.map