@nifrajs/web 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +3 -3
  2. package/dist/build-vite.d.ts +68 -0
  3. package/dist/build-vite.d.ts.map +1 -0
  4. package/dist/build-vite.js +433 -0
  5. package/dist/build-vite.js.map +1 -0
  6. package/dist/build.d.ts +174 -65
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +376 -135
  9. package/dist/build.js.map +1 -1
  10. package/dist/bun-dev-entry.d.ts +83 -0
  11. package/dist/bun-dev-entry.d.ts.map +1 -0
  12. package/dist/bun-dev-entry.js +136 -0
  13. package/dist/bun-dev-entry.js.map +1 -0
  14. package/dist/chunk-names.js +1 -1
  15. package/dist/client.d.ts +3 -9
  16. package/dist/client.d.ts.map +1 -1
  17. package/dist/client.js +209 -27
  18. package/dist/client.js.map +1 -1
  19. package/dist/conformance.d.ts +2 -2
  20. package/dist/conformance.js +3 -3
  21. package/dist/deferred.d.ts +11 -11
  22. package/dist/deferred.d.ts.map +1 -1
  23. package/dist/deferred.js +16 -16
  24. package/dist/dev-error.d.ts +2 -2
  25. package/dist/dev-error.js +3 -3
  26. package/dist/dev-port.d.ts +36 -0
  27. package/dist/dev-port.d.ts.map +1 -0
  28. package/dist/dev-port.js +48 -0
  29. package/dist/dev-port.js.map +1 -0
  30. package/dist/dev.d.ts +62 -5
  31. package/dist/dev.d.ts.map +1 -1
  32. package/dist/dev.js +419 -60
  33. package/dist/dev.js.map +1 -1
  34. package/dist/draft.d.ts +44 -7
  35. package/dist/draft.d.ts.map +1 -1
  36. package/dist/draft.js +92 -9
  37. package/dist/draft.js.map +1 -1
  38. package/dist/fn-state.d.ts +56 -0
  39. package/dist/fn-state.d.ts.map +1 -0
  40. package/dist/fn-state.js +77 -0
  41. package/dist/fn-state.js.map +1 -0
  42. package/dist/fn.d.ts +111 -0
  43. package/dist/fn.d.ts.map +1 -0
  44. package/dist/fn.js +125 -0
  45. package/dist/fn.js.map +1 -0
  46. package/dist/fonts-google.d.ts +9 -9
  47. package/dist/fonts-google.js +10 -10
  48. package/dist/fonts.d.ts +7 -7
  49. package/dist/fonts.js +1 -1
  50. package/dist/forms.d.ts +8 -8
  51. package/dist/fs.d.ts +15 -2
  52. package/dist/fs.d.ts.map +1 -1
  53. package/dist/fs.js +8 -2
  54. package/dist/fs.js.map +1 -1
  55. package/dist/index.d.ts +266 -74
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +869 -179
  58. package/dist/index.js.map +1 -1
  59. package/dist/internal/head-attributes.d.ts +15 -0
  60. package/dist/internal/head-attributes.d.ts.map +1 -0
  61. package/dist/internal/head-attributes.js +99 -0
  62. package/dist/internal/head-attributes.js.map +1 -0
  63. package/dist/internal/script-types.d.ts +14 -0
  64. package/dist/internal/script-types.d.ts.map +1 -0
  65. package/dist/internal/script-types.js +17 -0
  66. package/dist/internal/script-types.js.map +1 -0
  67. package/dist/internal/server-boundary.d.ts +60 -0
  68. package/dist/internal/server-boundary.d.ts.map +1 -0
  69. package/dist/internal/server-boundary.js +174 -0
  70. package/dist/internal/server-boundary.js.map +1 -0
  71. package/dist/internal/timing-safe-equal.d.ts +16 -0
  72. package/dist/internal/timing-safe-equal.d.ts.map +1 -0
  73. package/dist/internal/timing-safe-equal.js +26 -0
  74. package/dist/internal/timing-safe-equal.js.map +1 -0
  75. package/dist/internal/vite-import.d.ts +63 -0
  76. package/dist/internal/vite-import.d.ts.map +1 -0
  77. package/dist/internal/vite-import.js +94 -0
  78. package/dist/internal/vite-import.js.map +1 -0
  79. package/dist/islands.d.ts +5 -5
  80. package/dist/islands.js +5 -5
  81. package/dist/isr.d.ts +15 -15
  82. package/dist/isr.d.ts.map +1 -1
  83. package/dist/isr.js +8 -21
  84. package/dist/isr.js.map +1 -1
  85. package/dist/llms-txt.d.ts.map +1 -1
  86. package/dist/llms-txt.js +12 -6
  87. package/dist/llms-txt.js.map +1 -1
  88. package/dist/manifest.d.ts +121 -40
  89. package/dist/manifest.d.ts.map +1 -1
  90. package/dist/manifest.js +136 -26
  91. package/dist/manifest.js.map +1 -1
  92. package/dist/module-graph.d.ts +91 -0
  93. package/dist/module-graph.d.ts.map +1 -0
  94. package/dist/module-graph.js +92 -0
  95. package/dist/module-graph.js.map +1 -0
  96. package/dist/navigation.d.ts +120 -5
  97. package/dist/navigation.d.ts.map +1 -1
  98. package/dist/navigation.js +37 -5
  99. package/dist/navigation.js.map +1 -1
  100. package/dist/plugins/css-modules.d.ts +3 -3
  101. package/dist/plugins/css-modules.d.ts.map +1 -1
  102. package/dist/plugins/css-modules.js +0 -0
  103. package/dist/plugins/css-modules.js.map +1 -1
  104. package/dist/plugins/kit.d.ts +8 -8
  105. package/dist/plugins/kit.js +9 -9
  106. package/dist/plugins/scss.js +3 -3
  107. package/dist/plugins/vite-leak-guard.d.ts +45 -0
  108. package/dist/plugins/vite-leak-guard.d.ts.map +1 -0
  109. package/dist/plugins/vite-leak-guard.js +67 -0
  110. package/dist/plugins/vite-leak-guard.js.map +1 -0
  111. package/dist/plugins/vite-server-fn.d.ts +23 -0
  112. package/dist/plugins/vite-server-fn.d.ts.map +1 -0
  113. package/dist/plugins/vite-server-fn.js +51 -0
  114. package/dist/plugins/vite-server-fn.js.map +1 -0
  115. package/dist/plugins/vite-server-only.d.ts +49 -0
  116. package/dist/plugins/vite-server-only.d.ts.map +1 -0
  117. package/dist/plugins/vite-server-only.js +57 -0
  118. package/dist/plugins/vite-server-only.js.map +1 -0
  119. package/dist/prerender.d.ts +5 -5
  120. package/dist/prerender.js +7 -7
  121. package/dist/public-dir.d.ts +36 -0
  122. package/dist/public-dir.d.ts.map +1 -0
  123. package/dist/public-dir.js +89 -0
  124. package/dist/public-dir.js.map +1 -0
  125. package/dist/query.d.ts +10 -10
  126. package/dist/query.js +15 -15
  127. package/dist/route-manifest.d.ts +91 -0
  128. package/dist/route-manifest.d.ts.map +1 -0
  129. package/dist/route-manifest.js +128 -0
  130. package/dist/route-manifest.js.map +1 -0
  131. package/dist/router.d.ts +94 -22
  132. package/dist/router.d.ts.map +1 -1
  133. package/dist/router.js +170 -58
  134. package/dist/router.js.map +1 -1
  135. package/dist/search.d.ts +94 -0
  136. package/dist/search.d.ts.map +1 -0
  137. package/dist/search.js +232 -0
  138. package/dist/search.js.map +1 -0
  139. package/dist/server-only.d.ts +6 -6
  140. package/dist/server-only.js +7 -7
  141. package/dist/service-worker.d.ts +56 -0
  142. package/dist/service-worker.d.ts.map +1 -0
  143. package/dist/service-worker.js +127 -0
  144. package/dist/service-worker.js.map +1 -0
  145. package/dist/vite.d.ts +25 -17
  146. package/dist/vite.d.ts.map +1 -1
  147. package/dist/vite.js +75 -33
  148. package/dist/vite.js.map +1 -1
  149. package/package.json +48 -4
  150. package/server-manifest.d.ts +31 -0
  151. package/src/build-vite.ts +553 -0
  152. package/src/build.ts +563 -172
  153. package/src/bun-dev-entry.ts +184 -0
  154. package/src/chunk-names.ts +1 -1
  155. package/src/client.ts +221 -30
  156. package/src/conformance.ts +3 -3
  157. package/src/deferred.ts +17 -17
  158. package/src/dev-error.ts +3 -3
  159. package/src/dev-port.ts +63 -0
  160. package/src/dev.ts +468 -66
  161. package/src/draft.ts +124 -12
  162. package/src/fn-state.ts +107 -0
  163. package/src/fn.ts +199 -0
  164. package/src/fonts-google.ts +13 -13
  165. package/src/fonts.ts +7 -7
  166. package/src/forms.ts +8 -8
  167. package/src/fs.ts +24 -6
  168. package/src/index.ts +1174 -225
  169. package/src/internal/head-attributes.ts +109 -0
  170. package/src/internal/script-types.ts +18 -0
  171. package/src/internal/server-boundary.ts +187 -0
  172. package/src/internal/timing-safe-equal.ts +24 -0
  173. package/src/internal/vite-import.ts +92 -0
  174. package/src/islands.ts +7 -7
  175. package/src/isr.ts +18 -30
  176. package/src/llms-txt.ts +13 -5
  177. package/src/manifest.ts +264 -48
  178. package/src/module-graph.ts +146 -0
  179. package/src/navigation.ts +151 -5
  180. package/src/plugins/css-modules.ts +0 -0
  181. package/src/plugins/kit.ts +11 -11
  182. package/src/plugins/scss.ts +3 -3
  183. package/src/plugins/vite-leak-guard.ts +110 -0
  184. package/src/plugins/vite-server-fn.ts +67 -0
  185. package/src/plugins/vite-server-only.ts +75 -0
  186. package/src/prerender.ts +10 -10
  187. package/src/public-dir.ts +108 -0
  188. package/src/query.ts +22 -22
  189. package/src/route-manifest.ts +240 -0
  190. package/src/router.ts +288 -74
  191. package/src/search.ts +297 -0
  192. package/src/server-only.ts +7 -7
  193. package/src/service-worker.ts +165 -0
  194. package/src/vite.ts +103 -49
package/dist/index.d.ts CHANGED
@@ -1,41 +1,42 @@
1
1
  /**
2
- * @nifrajs/web — the framework-agnostic SSR core. It owns the render *seam* and the HTML
2
+ * @nifrajs/web - the framework-agnostic SSR core. It owns the render *seam* and the HTML
3
3
  * document orchestration, and knows nothing about any specific UI framework: a render
4
4
  * adapter (@nifrajs/web-solid, @nifrajs/web-react, …) supplies the actual render + hydrate.
5
5
  *
6
- * The core treats a "component" and a hydration "container" as opaque `unknown` — only
6
+ * The core treats a "component" and a hydration "container" as opaque `unknown` - only
7
7
  * the adapter interprets them. That keeps this package both framework-agnostic and free
8
8
  * of DOM types: it is pure server orchestration + string building.
9
9
  */
10
10
  import { server } from "@nifrajs/core/server";
11
11
  import { type Deferred, defer } from "./deferred.js";
12
- import type { LinkDescriptor, Manifest, Meta, MetaArgs, MetaInput, ScriptDescriptor } from "./manifest.js";
12
+ import type { LinkDescriptor, Manifest, Meta, MetaArgs, MetaDescriptor, MetaInput, ScriptDescriptor, UnsafeScriptDescriptor } from "./manifest.js";
13
13
  import { type Submission } from "./router.js";
14
- export { DRAFT_COOKIE, type DraftCookieControls, disableDraft, type EnableDraftOptions, enableDraft, isDraftEnabled, } from "./draft.js";
14
+ export { DRAFT_COOKIE, type DraftCookieControls, disableDraft, type EnableDraftOptions, enableDraft, isDraftEnabled, type PreviewEndpointOptions, previewEndpoint, } from "./draft.js";
15
15
  export { type FontDisplay, type FontFace, type FontPreloadInput, type FontSource, fontFace, fontPreload, } from "./fonts.js";
16
16
  export { type CachedResponse, type CacheStore, ISR_REVALIDATE_HEADER, ISR_STATUS_HEADER, type ISRApp, type ISROptions, type ISRPlatform, KVCacheStore, type KVCacheStoreOptions, type KVNamespaceLike, MemoryCacheStore, type MemoryCacheStoreOptions, type RevalidateEndpointOptions, revalidateEndpoint, withISR, } from "./isr.js";
17
- export { type Action, buildManifest, enumerateStaticRoutes, filePathToPattern, filePathToPatterns, type GetStaticPaths, type LayoutEntry, type LinkDescriptor, type Loader, type LoaderContext, type Manifest, type Meta, type MetaArgs, type MetaInput, type RouteEntry, type RouteModule, type ScriptDescriptor, type StaticPath, type StaticPaths, type StaticRoutes, } from "./manifest.js";
18
- export { type BrowserNavigate, getBrowserNavigate, type NavigateOptions, setBrowserNavigate, } from "./navigation.js";
17
+ export { type Action, buildManifest, enumerateStaticRoutes, filePathToPattern, filePathToPatterns, type GetStaticPaths, type InertScriptType, type LayoutEntry, type LinkDescriptor, type Loader, type LoaderContext, type Manifest, type Meta, type MetaArgs, type MetaInput, type RouteEntry, type RouteModule, type ScriptDescriptor, type StaticPath, type StaticPaths, type StaticRoutes, type UnsafeScriptDescriptor, } from "./manifest.js";
18
+ export { type Blocker, type BlockerController, type BlockerFunction, type BlockerLocation, type BlockerState, type BrowserNavigate, getBrowserNavigate, IDLE_BLOCKER, type NavigateFunction, type NavigateOptions, type NavigateSearchOf, type NavigateTargetInput, type RouteSearch, registerBlocker, resolveNavigate, setBlockerController, setBrowserNavigate, } from "./navigation.js";
19
+ export { type PublicDirCache, resolvePublicPath, type ServePublicDirOptions, servePublicDir, } from "./public-dir.js";
19
20
  export { createMutation, createQueryClient, type DehydratedState, hashQueryKey, type InfiniteData, type InfiniteQueryHandle, type InfiniteQueryOptions, type MutationCallbacks, type MutationHandle, type MutationState, type MutationStatus, type QueryClient, type QueryClientOptions, type QueryHandle, type QueryOptions, type QueryState, type QueryStatus, } from "./query.js";
20
- export { type ClientRouter, type ClientRouterOptions, createClientRouter, createMatcher, DATA_HEADER, type Fetcher, type FetcherState, type FetchRouteData, type MountRouterOptions, REDIRECT_HEADER, REVALIDATE_HEADER, type RouteMatch, type RoutePattern, type RouterState, type Submission, type SubmitOptions, } from "./router.js";
21
+ export { type ClientRouter, type ClientRouterOptions, createClientRouter, createMatcher, DATA_HEADER, type Fetcher, type FetcherState, type FetchRouteData, type MountRouterOptions, NAV_FROM_HEADER, REDIRECT_HEADER, RETAIN_HEADER, REVALIDATE_HEADER, type RouteMatch, type RoutePattern, type RouterState, STATUS_HEADER, type Submission, type SubmitOptions, } from "./router.js";
21
22
  export { type Deferred, defer };
22
23
  /** Phantom brand key for {@link ServerOnly}. A unique symbol so the brand can't be forged by a plain
23
24
  * object literal; `unique symbol` keys never appear in the emitted JS, so the brand is purely
24
25
  * type-level (zero runtime cost). The property is optional + `never`-typed, so a `ServerOnly<T>`
25
- * stays assignment-compatible with `T` — it documents intent without obstructing real use. */
26
+ * stays assignment-compatible with `T` - it documents intent without obstructing real use. */
26
27
  declare const SERVER_ONLY_BRAND: unique symbol;
27
28
  /**
28
- * Type-level intent marker for a value that must only exist on the server — a secret, a DB handle, a
29
+ * Type-level intent marker for a value that must only exist on the server - a secret, a DB handle, a
29
30
  * server-only client. `ServerOnly<T>` is structurally `T` (the brand is an optional phantom field, so
30
31
  * existing code keeps type-checking), but it advertises to readers + the compiler that the value is
31
- * not meant to cross to the browser. It is **purely type-level** and erases at build — it does NOT,
32
+ * not meant to cross to the browser. It is **purely type-level** and erases at build - it does NOT,
32
33
  * by itself, keep the value out of the client bundle.
33
34
  *
34
35
  * The enforcement is two runtime conventions:
35
- * - add the side-effect import `import "@nifrajs/web/server-only"` at the top of the module — the
36
+ * - add the side-effect import `import "@nifrajs/web/server-only"` at the top of the module - the
36
37
  * client build ({@link buildClient}) fails loud, with the import chain, if it reaches a browser
37
38
  * chunk (the poison-import marker);
38
- * - or name the file `*.server.ts` — the `.server` convention empties it in the client build.
39
+ * - or name the file `*.server.ts` - the `.server` convention empties it in the client build.
39
40
  *
40
41
  * Use this brand to express the intent in the types; pair it with one of those runtime markers so a
41
42
  * leak is caught at build time rather than shipping a secret to the client.
@@ -49,11 +50,20 @@ export type ServerOnly<T> = T & {
49
50
  readonly [SERVER_ONLY_BRAND]?: never;
50
51
  };
51
52
  export { assertRenderAdapterConformance, RenderAdapterConformanceError, type RenderAdapterConformanceFixture, } from "./conformance.js";
53
+ export { type SearchOf, searchOf, searchOfChain, serializeSearch } from "./search.js";
52
54
  /** The data handed to a route component. Opaque to the core. `actionData` is the return of a
53
55
  * route `action` after a POST (absent on plain GETs). `pending` + `submission` are client-only
54
- * (absent on SSR): they drive **optimistic UI** — render from `submission.formData` while `pending`. */
56
+ * (absent on SSR): they drive **optimistic UI** - render from `submission.formData` while `pending`. */
55
57
  export interface RenderProps {
56
58
  readonly data: unknown;
59
+ /**
60
+ * Per-layout loader data, aligned index-for-index with the layout prefix of `chain`.
61
+ *
62
+ * Absent when no layout in the chain exports a `loader`, so a page-only app is byte-identical to
63
+ * before. An adapter that does not read it renders exactly as it did; one that does passes
64
+ * `layoutData[i]` to `chain[i]`.
65
+ */
66
+ readonly layoutData?: readonly unknown[];
57
67
  readonly actionData?: unknown;
58
68
  /** True while a client navigation or submit is in flight (client-only; absent/false on SSR). */
59
69
  readonly pending?: boolean;
@@ -66,10 +76,15 @@ export interface RenderProps {
66
76
  * SSR (from the request match) and on client navigation (from router state) so an adapter's
67
77
  * `useParams` reads the same value on both, with no hydration mismatch. Absent ⇒ no params (`{}`). */
68
78
  readonly params?: Readonly<Record<string, string>>;
69
- /** The current URL's `pathname + search` (no hash — the hash never reaches the server). Threaded on
79
+ /** The current URL's `pathname + search` (no hash - the hash never reaches the server). Threaded on
70
80
  * SSR and client alike so an adapter's `useLocation`/`useSearchParams` hydrate consistently. Absent
71
81
  * on adapters/callers that don't supply it (treated as `""`). */
72
82
  readonly path?: string;
83
+ /** The route's typed, validated search params (the value the loader also gets as `ctx.search`),
84
+ * threaded to the adapter's `useSearch`. Derived identically on SSR (from the request) and on client
85
+ * navigation (adapter mount) via the shared `searchOf`, so a component reading it hydrates with no
86
+ * drift. Absent ⇒ no search context (`{}`). */
87
+ readonly search?: Record<string, unknown>;
73
88
  }
74
89
  /**
75
90
  * The seam every render adapter implements. New adapters should prove these invariants with
@@ -82,17 +97,17 @@ export interface RenderAdapter {
82
97
  * (the loader data); each layout wraps the child via its `children`. Returns (or resolves to)
83
98
  * a Web `ReadableStream<Uint8Array>`: a streaming renderer flushes the shell first and streams
84
99
  * Suspense boundaries as they resolve; a non-streaming renderer may return a one-chunk stream.
85
- * May be async — e.g. React resolves once the shell is renderable, so a shell-render throw can
100
+ * May be async - e.g. React resolves once the shell is renderable, so a shell-render throw can
86
101
  * still map to an error status before any byte is sent.
87
102
  */
88
103
  renderToStream(chain: readonly unknown[], props: RenderProps): ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>;
89
104
  /**
90
105
  * Server (optional): render the chain to a complete HTML **string** in one pass, with the same
91
106
  * hydration markers `renderToStream` produces. When provided, `renderPage` uses it for any page
92
- * that does **not** `defer()` — buffering the document is faster than the streaming pipeline there,
107
+ * that does **not** `defer()` - buffering the document is faster than the streaming pipeline there,
93
108
  * because the framework's synchronous renderer (React `renderToString`, Solid `renderToString`,
94
109
  * Preact `render`, Vue `renderToString`, Svelte `render`) is markedly cheaper than its streaming
95
- * renderer — most visibly on Bun, where React Fizz / Solid `renderToStream` are the heaviest. Pages
110
+ * renderer - most visibly on Bun, where React Fizz / Solid `renderToStream` are the heaviest. Pages
96
111
  * that `defer()` keep the streaming path (progressive `<Await>` needs it). Markup MUST be
97
112
  * hydration-equivalent to `renderToStream`; a throw surfaces like the streaming shell-readiness
98
113
  * await (the `_error` boundary maps it to a status). Omit it to always stream.
@@ -102,22 +117,37 @@ export interface RenderAdapter {
102
117
  * Server: per-document bootstrap markup injected into `<head>` that the client
103
118
  * `hydrate` requires (Solid: `generateHydrationScript()`). Empty string if none.
104
119
  */
105
- hydrationHead(): string;
120
+ hydrationHead(nonce?: string): string;
106
121
  }
107
122
  /** Global the server serializes loader data into; the client reads it to hydrate. */
108
123
  export declare const DATA_GLOBAL = "__NIFRA_DATA__";
124
+ /** Per-layout loader data for hydration. Emitted ONLY when some layout in the chain has a loader, so
125
+ * a page-only app's HTML is byte-identical to before layout loaders existed. */
126
+ export declare const LAYOUT_DATA_GLOBAL = "__NIFRA_LAYOUT_DATA__";
109
127
  /** Global the server writes the matched route id into; the client uses it to pick the chain. */
110
128
  export declare const ROUTE_GLOBAL = "__NIFRA_ROUTE__";
111
129
  /** Global the server serializes an action's data return into (absent on GETs); the client
112
130
  * reads it so hydration after a native form POST matches the server-rendered markup. */
113
131
  export declare const ACTION_GLOBAL = "__NIFRA_ACTION__";
114
132
  /**
115
- * Pre-hydration form guard — a tiny inline script flushed in `<head>` (it runs in the window between
133
+ * Marker attribute on the app container, emitted ONLY when `rootId` is not the default.
134
+ *
135
+ * `rootId` is a per-RENDER option; the client entry is a per-BUILD artifact, so it cannot be
136
+ * generated knowing which id a given render will use. The container therefore announces itself, and
137
+ * the entry finds it by this attribute before falling back to `#root`. It has to be the DOM rather
138
+ * than another `window.__NIFRA_*` global: a second copy of the id can drift from the markup, while
139
+ * this is written by the same expression that writes the id and cannot.
140
+ *
141
+ * Absent on a default render, so an app that never sets `rootId` emits the same bytes it always has.
142
+ */
143
+ export declare const ROOT_ATTRIBUTE = "data-nifra-root";
144
+ /**
145
+ * Pre-hydration form guard - a tiny inline script flushed in `<head>` (it runs in the window between
116
146
  * first paint and the island bundle taking over). It neutralizes the one real hydration footgun: a
117
147
  * JS-only form (a hand-wired `onSubmit` with no native fallback) submitting *natively* before its
118
148
  * handler is attached, which navigates the browser to a broken `?field=…` GET of the current page.
119
149
  *
120
- * It blocks ONLY that shape — an effective-GET form whose action targets the current document — and
150
+ * It blocks ONLY that shape - an effective-GET form whose action targets the current document - and
121
151
  * only until hydration commits (`data-nifra-hydrated`, set by the client entry). Method-`post` forms
122
152
  * (progressive enhancement: the native POST hits the route `action`) and GET forms with a real action
123
153
  * (a search box → `/search`) pass through untouched. Opt out per-form with `data-native`. Static text
@@ -127,7 +157,7 @@ export declare const PRE_HYDRATION_GUARD: string;
127
157
  /**
128
158
  * The single default port for the dev server (`@nifrajs/web/dev`, `@nifrajs/web/vite`) **and**
129
159
  * `nifra start`. Deliberately uncommon: `3000`/`5173`/`8080` collide with whatever else is running
130
- * (Next, Vite, a stray Node API). `4321` rarely is — and being the *same* constant across `nifra dev`
160
+ * (Next, Vite, a stray Node API). `4321` rarely is - and being the *same* constant across `nifra dev`
131
161
  * and `nifra start` means a project's URL doesn't change between commands. Override per-run with
132
162
  * `--port <n>` or the `PORT` env var. */
133
163
  export declare const DEFAULT_DEV_PORT = 4321;
@@ -144,21 +174,27 @@ export interface RenderedPage {
144
174
  }
145
175
  export interface RenderPageOptions {
146
176
  readonly adapter: RenderAdapter;
147
- /** The layout chain to render — outermost layout → page (opaque; the adapter renders it). */
177
+ /** The layout chain to render - outermost layout → page (opaque; the adapter renders it). */
148
178
  readonly chain: readonly unknown[];
149
179
  /** The loader output for this request. */
150
180
  readonly data: unknown;
151
- /** An action's data return (POST only) — surfaced to the page as `actionData` + serialized
181
+ /** An action's data return (POST only) - surfaced to the page as `actionData` + serialized
152
182
  * for the client so post-POST hydration matches. Omit on GETs. */
153
183
  readonly actionData?: unknown;
154
- /** URL of the built client entry (loaded as a module script). */
155
- readonly clientEntry: string;
184
+ /**
185
+ * URL of the built client entry (loaded as a module script).
186
+ *
187
+ * Optional ONLY on a `hydrate: false` page, where there is no client takeover to feed and this value
188
+ * reaches no output at all. The union below is what enforces that: omitting it on a hydrating page
189
+ * stays a compile error rather than rendering `src=""`.
190
+ */
191
+ readonly clientEntry?: string;
156
192
  /** Chunk URLs for the **matched** route (its layout chain + own chunk) to `modulepreload` in the
157
- * shell — so the route code downloads in parallel with the entry instead of after it
193
+ * shell - so the route code downloads in parallel with the entry instead of after it
158
194
  * (`buildClient`'s per-route map). Empty/omitted ⇒ only the entry is preloaded (unchanged). */
159
195
  readonly preload?: readonly string[];
160
196
  /** Stylesheet URLs for the **matched** route (its layout chain + own CSS, from `buildClient`'s
161
- * `BuildManifest.css`) — injected as `<link rel="stylesheet">` in `<head>` so styles arrive with the
197
+ * `BuildManifest.css`) - injected as `<link rel="stylesheet">` in `<head>` so styles arrive with the
162
198
  * first paint (no FOUC). Rendered even on non-hydrated pages. Empty/omitted ⇒ none (unchanged). */
163
199
  readonly styles?: readonly string[];
164
200
  /** SSG: the prerendered-path set, serialized to `window.__NIFRA_PRERENDERED__` so the client fetches
@@ -169,52 +205,78 @@ export interface RenderPageOptions {
169
205
  readonly revalidate?: number;
170
206
  /** Matched route id; written to `window.__NIFRA_ROUTE__` so the client hydrates this chain. */
171
207
  readonly routeId?: string;
172
- /** The matched route's decoded path params — surfaced to the page as `params` (via {@link RenderProps})
208
+ /** The matched route's decoded path params - surfaced to the page as `params` (via {@link RenderProps})
173
209
  * so an adapter's `useParams` is SSR-correct. Omit ⇒ `{}` (a route with no dynamic segments, or an
174
210
  * error/404 render). */
175
211
  readonly params?: Readonly<Record<string, string>>;
176
- /** The request's `pathname + search` — surfaced as `path` (via {@link RenderProps}) so an adapter's
212
+ /** The request's `pathname + search` - surfaced as `path` (via {@link RenderProps}) so an adapter's
177
213
  * `useLocation`/`useSearchParams` render the right URL server-side and hydrate without drift. Omit ⇒
178
214
  * `""`. */
179
215
  readonly path?: string;
216
+ /** The route's validated search (`searchOf(mod.searchSchema, url.search)`, the same value handed to
217
+ * the loader as `ctx.search`), surfaced as `search` (via {@link RenderProps}) so an adapter's
218
+ * `useSearch` is SSR-correct. Omit ⇒ `{}` (a render with no search context). */
219
+ readonly search?: Record<string, unknown>;
220
+ /** Per-layout loader data, forwarded to the adapter as `RenderProps.layoutData`. Aligned with the
221
+ * chain's layout prefix; omitted when no layout in the chain has a loader. */
222
+ readonly layoutData?: readonly unknown[];
180
223
  /** HTTP status for the response (default 200; e.g. 404 for a not-found page). */
181
224
  readonly status?: number;
225
+ /** Extra response headers - e.g. the `cache-control` a terminal status page wants. `content-type`
226
+ * is ignored (the document is always HTML) and the ISR freshness header is applied after these,
227
+ * so neither can be overridden from here. */
228
+ readonly headers?: HeadersLike;
182
229
  /** Document title (fallback when `head.title` is unset). */
183
230
  readonly title?: string;
184
- /** Resolved route head — `title` overrides `title` above; `meta`/`link` render as managed
231
+ /** Resolved route head - `title` overrides `title` above; `meta`/`link` render as managed
185
232
  * (`data-nifra`) tags the client updates on navigation.
186
233
  *
187
234
  * **Head contract (the layout chain contributes).** `createWebApp` resolves this via
188
235
  * {@link mergeHeads}: a route's head is its **layout chain's** `meta`/`head` exports merged with the
189
- * page's. A `_layout.tsx` may `export const meta` (or `export function meta(args)`) — its tags land
236
+ * page's. A `_layout.tsx` may `export const meta` (or `export function meta(args)`) - its tags land
190
237
  * on every page below it (the home for `hreflang`, `preconnect`, a section-default `<title>`). The
191
238
  * merge is **nearest-wins for scalars** (the page's `title` overrides an inner layout's, which
192
239
  * overrides an outer one; an undefined page title keeps the layout's) and **concatenated for the
193
- * `meta`/`link` arrays** (outermost layout first, page last). `<link>` attributes are name-validated
194
- * (any letter/digit/hyphen name — covers `rel`/`href`/`hreflang`/`crossorigin`/`media`/`sizes`/`as`/
195
- * `integrity`/`fetchpriority`/…) and value-escaped against XSS. */
240
+ * `meta`/`link` arrays** (outermost layout first, page last). `<meta>`/`<link>` attributes pass a
241
+ * shared tag-specific allowlist (including inert `data-*` metadata) and values are escaped. */
196
242
  readonly head?: Meta;
197
- /** Id of the container wrapping the app markup (default `"root"`). */
243
+ /** Id of the container wrapping the app markup (default `"root"`). A non-default id also gets
244
+ * {@link ROOT_ATTRIBUTE}, which is how the generated client entry finds the container to hydrate -
245
+ * the entry is built once and cannot know what a given render chose. */
198
246
  readonly rootId?: string;
199
- /** When `false`, emit a complete but **non-hydrated** document — no client entry script, data
247
+ /** When `false`, emit a complete but **non-hydrated** document - no client entry script, data
200
248
  * globals, or modulepreloads. Used for server-rendered `_error` pages: a terminal state that needs no
201
249
  * client takeover, and it sidesteps an SSR/hydrate mismatch (the server rendered the boundary, not
202
250
  * the page the client manifest maps this route id to). Default `true`. */
203
251
  readonly hydrate?: boolean;
204
252
  /** Island client bundles (`@nifrajs/web/islands`) to load as `<script type="module">` in the document
205
- * tail — emitted **regardless of `hydrate`**, so a static (`hydrate: false`) page can still mount
253
+ * tail - emitted **regardless of `hydrate`**, so a static (`hydrate: false`) page can still mount
206
254
  * no-framework islands. URLs are attribute-escaped. Empty/omitted ⇒ none (unchanged output). */
207
255
  readonly islandScripts?: readonly string[];
256
+ /** CSP nonce applied to every framework-owned executable script in this document. */
257
+ readonly nonce?: string;
208
258
  }
209
259
  /**
210
- * Server: render a full HTML document for a page — the adapter's hydration head + the SSR
211
- * markup (**streamed**) + the serialized loader data + the client module — as a `Response`.
260
+ * `renderPage` input. A hydrating page (the default) must supply `clientEntry`, because the document
261
+ * loads it as a module script; a `hydrate: false` page may omit it, because nothing in the emitted
262
+ * document references it. Expressed as a union so the compiler enforces the pairing rather than the
263
+ * renderer discovering an empty `src` at runtime.
264
+ */
265
+ export type RenderPageInput = RenderPageOptions & ({
266
+ readonly hydrate?: true;
267
+ readonly clientEntry: string;
268
+ } | {
269
+ readonly hydrate: false;
270
+ });
271
+ /**
272
+ * Server: render a full HTML document for a page - the adapter's hydration head + the SSR
273
+ * markup (**streamed**) + the serialized loader data + the client module - as a `Response`.
212
274
  * The shell (`<head>` + the open container) flushes first, the adapter's app stream follows,
213
275
  * then the tail (data globals + client entry). Pure Web Standards, so it returns straight from
214
276
  * a nifra route handler and streams on any fetch runtime (Bun/Node/Deno/Workers).
215
277
  */
216
- export declare function renderPage(options: RenderPageOptions): MaybePromise<Response>;
217
- export declare function renderPageResult(options: RenderPageOptions): MaybePromise<RenderedPage>;
278
+ export declare function renderPage(options: RenderPageInput): MaybePromise<Response>;
279
+ export declare function renderPageResult(options: RenderPageInput): MaybePromise<RenderedPage>;
218
280
  /** Options for {@link redirect}. */
219
281
  export interface RedirectOptions {
220
282
  /** HTTP status (default 303 See Other; pass 307/308 to preserve the method). */
@@ -225,21 +287,76 @@ export interface RedirectOptions {
225
287
  readonly external?: boolean;
226
288
  }
227
289
  /**
228
- * Build a redirect `Response` — return it from a route `action` for the Post/Redirect/Get
290
+ * Build a redirect `Response` - return it from a route `action` for the Post/Redirect/Get
229
291
  * pattern (POST mutates, 303 sends the browser to a fresh GET, so a reload doesn't re-submit).
230
292
  * Defaults to 303 (See Other); pass `{ status: 307 }` or `{ status: 308 }` to preserve the method.
231
293
  *
232
294
  * **Secure by default:** `location` must be a same-origin path (begins with `/`, not `//`). An
233
- * off-origin/absolute destination throws unless you pass `{ external: true }` — this closes the
295
+ * off-origin/absolute destination throws unless you pass `{ external: true }` - this closes the
234
296
  * open-redirect footgun of `return redirect(formData.get("next"))` on the no-JS (native-form) path,
235
297
  * which returns the action's `Response` verbatim.
236
298
  *
237
299
  * @param options redirect status and whether an off-origin destination is intentional.
238
300
  */
239
301
  export declare function redirect(location: string, options?: RedirectOptions): Response;
302
+ /**
303
+ * Brand marking a `Response` as a **terminal-status signal** rather than a response to serve verbatim.
304
+ *
305
+ * A loader that throws a bare `Response` has always been passed straight through - that is how
306
+ * `throw redirect(...)` works, and apps rely on it. So "render the `_404` boundary at 404" cannot be
307
+ * expressed by throwing a plain `Response`: the framework would have no way to tell it apart from a
308
+ * hand-rolled body the app wants served exactly as written. The brand is that distinction, and
309
+ * checking it *before* the pass-through is what keeps every existing throw working unchanged.
310
+ *
311
+ * `Symbol.for` matches `RESPONSE_RESULT` above: a registry symbol survives two copies of this module,
312
+ * which a `unique symbol` would not - and a duplicate-install turning a 404 into a raw 404 body would
313
+ * be a maddening bug to trace.
314
+ */
315
+ /** Derived from the `Headers` constructor rather than the DOM lib's `HeadersInit`: `@nifrajs/web`
316
+ * is consumed from DOM-free programs (the root tsconfig runs `types: ["bun"]`), and naming the DOM
317
+ * type here would break their build. Same workaround as `SSEInit.headers` in core. */
318
+ type HeadersLike = ConstructorParameters<typeof Headers>[0];
319
+ /** Options shared by {@link notFound}, {@link gone}, and {@link statusPage}. */
320
+ export interface StatusPageOptions {
321
+ /**
322
+ * Extra headers for the rendered response - `cache-control` above all.
323
+ *
324
+ * The defaults differ by status for a reason: a 404 may be a race with publication, so it wants a
325
+ * short TTL, while a 410 is a promise that the URL is permanently gone and can be cached hard.
326
+ * Getting this wrong is the difference between a crawler dropping a dead URL and re-fetching it
327
+ * for weeks.
328
+ */
329
+ readonly headers?: HeadersLike;
330
+ }
331
+ /**
332
+ * Render the nearest `_404` page at status **404**. `throw` it from a loader when the record does not
333
+ * exist.
334
+ *
335
+ * This is the fix for the soft 404: a matched route whose loader finds nothing has otherwise no way to
336
+ * answer 404, so the path of least resistance is to return empty data and render "not found" inside a
337
+ * **200**. That looks correct in a browser and is invisible in review, which is why it ships and stays
338
+ * shipped - while search engines penalise it and keep the dead URL indexed.
339
+ *
340
+ * Returns `never`: these throw, so a loader narrows without a redundant `return`, and the type states
341
+ * the thing the "loaders `throw` redirect, actions `return` it" rule already trips people on.
342
+ */
343
+ export declare function notFound(options?: StatusPageOptions): never;
344
+ /**
345
+ * Render a terminal page at status **410 Gone**. `throw` it from a loader for a record that existed and
346
+ * was deliberately removed - a withdrawn listing, a deleted post.
347
+ *
348
+ * 410 is not a pedantic 404: it tells a crawler to **drop** the URL rather than re-fetch it for weeks
349
+ * on the assumption the 404 was transient. Uses `_410.tsx` if the app has one, otherwise `_404`.
350
+ */
351
+ export declare function gone(options?: StatusPageOptions): never;
352
+ /**
353
+ * Render a terminal page at any 4xx/5xx status - the escape hatch behind {@link notFound} and
354
+ * {@link gone} (402, 451, …). Uses `_<status>.tsx` if present, otherwise `_404`.
355
+ */
356
+ export declare function statusPage(status: number, options?: StatusPageOptions): never;
240
357
  /** The wrapper `revalidate()` returns: the action's `data` plus the paths it changed. A plain tagged
241
358
  * shape (not a class) so `@nifrajs/client`'s `ActionData` can unwrap it structurally without importing
242
- * from `@nifrajs/web`. `createWebApp` strips the wrapper — the client receives `data` as the body and
359
+ * from `@nifrajs/web`. `createWebApp` strips the wrapper - the client receives `data` as the body and
243
360
  * the paths via the `X-Nifra-Revalidate` header. */
244
361
  export interface RevalidateResult<T> {
245
362
  readonly __nifraRevalidate: readonly string[];
@@ -248,7 +365,7 @@ export interface RevalidateResult<T> {
248
365
  /**
249
366
  * Return this from an action to declare which routes the mutation changed (alongside the action's
250
367
  * `data`). `createWebApp` sets the `X-Nifra-Revalidate` response header; after the submit the client
251
- * marks those cached routes stale — refetching the active one and any mounted fetcher showing them —
368
+ * marks those cached routes stale - refetching the active one and any mounted fetcher showing them -
252
369
  * so a mutation can refresh views beyond the one that was submitted. `data` is still surfaced to the
253
370
  * component as `actionData` (the wrapper is transparent to `ActionData<typeof action>`).
254
371
  */
@@ -271,7 +388,7 @@ export declare function resolveMeta(meta: MetaInput | undefined, args: MetaArgs)
271
388
  * ```
272
389
  */
273
390
  export declare function canonical(href: string): LinkDescriptor;
274
- /** Inputs for {@link openGraph} — the common Open Graph properties. All optional; only the provided
391
+ /** Inputs for {@link openGraph} - the common Open Graph properties. All optional; only the provided
275
392
  * ones become tags. `type` defaults to `"website"`. */
276
393
  export interface OpenGraphInput {
277
394
  readonly title?: string;
@@ -280,7 +397,7 @@ export interface OpenGraphInput {
280
397
  readonly image?: string;
281
398
  /** Canonical URL of the page (`og:url`). */
282
399
  readonly url?: string;
283
- /** Object type (`og:type`) — `"website"`, `"article"`, … Default `"website"`. */
400
+ /** Object type (`og:type`) - `"website"`, `"article"`, … Default `"website"`. */
284
401
  readonly type?: string;
285
402
  }
286
403
  /**
@@ -291,7 +408,7 @@ export interface OpenGraphInput {
291
408
  * export const meta = { meta: [...openGraph({ title: "Nifra", image: "https://site.com/og.png" })] }
292
409
  * ```
293
410
  */
294
- export declare function openGraph(input: OpenGraphInput): Array<Record<string, string>>;
411
+ export declare function openGraph(input: OpenGraphInput): MetaDescriptor[];
295
412
  /**
296
413
  * Build a JSON-LD `<script type="application/ld+json">` entry for a route's `meta.script` from a plain
297
414
  * object. `JSON.stringify` produces the body; the head renderer breakout-escapes it (see
@@ -304,6 +421,14 @@ export declare function openGraph(input: OpenGraphInput): Array<Record<string, s
304
421
  * ```
305
422
  */
306
423
  export declare function jsonLd(data: Record<string, unknown>): ScriptDescriptor;
424
+ /**
425
+ * Deliberately unsafe escape hatch for executable inline code. The required nonce keeps the result
426
+ * compatible with a strict CSP and makes the security-sensitive choice visible at the call site.
427
+ */
428
+ export declare function unsafeInlineScript(content: string, options: {
429
+ readonly nonce: string;
430
+ readonly type?: "module" | "text/javascript";
431
+ }): UnsafeScriptDescriptor;
307
432
  /**
308
433
  * Merge a route's `<head>` contributions from its layout chain + the page into one {@link Meta}.
309
434
  *
@@ -311,15 +436,15 @@ export declare function jsonLd(data: Record<string, unknown>): ScriptDescriptor;
311
436
  * `meta` to put sitewide tags (`hreflang`, `preconnect`, a default `<title>`, …) on every page below
312
437
  * it. `heads` is passed **outermost layout → … → innermost layout → page** (the same order as the
313
438
  * render chain), and merges:
314
- * - **`title`** — *nearest-wins*: the last defined value across the list, so the page overrides an
439
+ * - **`title`** - *nearest-wins*: the last defined value across the list, so the page overrides an
315
440
  * inner layout, which overrides an outer one. A layout `title` is the section default; an undefined
316
441
  * page `title` keeps it.
317
- * - **`meta` / `link`** — *concatenated* in list order (outermost first, page last). Duplicate-tag
442
+ * - **`meta` / `link`** - *concatenated* in list order (outermost first, page last). Duplicate-tag
318
443
  * de-duplication is the caller's concern; the framework emits exactly what's declared so a layout
319
444
  * can ship N `<link rel="alternate" hreflang>` tags and a page can add its own canonical.
320
445
  *
321
446
  * Returns a fresh object whose identity is stable per `heads` *content* only when every entry is a
322
- * static (by-reference) `Meta` and there is exactly one — otherwise a new object each call. That is
447
+ * static (by-reference) `Meta` and there is exactly one - otherwise a new object each call. That is
323
448
  * fine: {@link headTags}'s memo is keyed on identity, so a per-request merge simply recomputes (its
324
449
  * content can vary with loader data anyway).
325
450
  */
@@ -331,7 +456,7 @@ export interface CreateWebAppOptions {
331
456
  readonly clientEntry: string;
332
457
  /** Default document title for all pages. */
333
458
  readonly title?: string;
334
- /** Injected into each loader's `ctx.api` — typically an `inProcessClient(app)` (typed
459
+ /** Injected into each loader's `ctx.api` - typically an `inProcessClient(app)` (typed
335
460
  * per-route via `@nifrajs/client`'s `createRoutes`). Opaque to the core.
336
461
  *
337
462
  * **Auto-mount.** Every `inProcessClient(backend)` exposes the symbol-keyed platform-aware backend
@@ -347,19 +472,48 @@ export interface CreateWebAppOptions {
347
472
  * auto-mount entirely (the app serves pages only and `api` stays a loader-only `ctx.api`). Mounting
348
473
  * is also a no-op when `api` does not expose the symbol mount. */
349
474
  readonly apiPrefix?: string;
475
+ /**
476
+ * Strip {@link apiPrefix} from the pathname before dispatching to `api` (default `false`).
477
+ *
478
+ * The default suits a backend that declares FULL paths (`server().post("/api/sync", …)`), which is
479
+ * right when it is only ever mounted here. Set this when the backend declares its routes WITHOUT the
480
+ * prefix because it also runs standalone behind its own shell, so its paths cannot carry a prefix that
481
+ * only exists when it is mounted. Without it every request 404s inside the backend, and the workaround
482
+ * is a `Proxy` that rewrites each URL.
483
+ */
484
+ readonly apiStrip?: boolean;
485
+ /**
486
+ * Sub-apps mounted ahead of page routing - an auth handler, a webhook receiver, a stack's routes.
487
+ *
488
+ * Structural on purpose: anything with `{ path, app: { fetch } }` fits, so a library that exposes its
489
+ * routes as such a list mounts as `mounts: theirRoutes` without `@nifrajs/web` taking a dependency on
490
+ * it. `better-auth` is the motivating case - it is not a `backend` route, so `/api/auth/*` used to 404
491
+ * silently.
492
+ *
493
+ * Tried longest-path-first and BEFORE the `api` mount, so a mount at `/api/auth` wins over a backend
494
+ * at `/api` no matter which was declared first. `stripPrefix` is the per-mount form of {@link apiStrip}:
495
+ * leave it off to pass the full path through.
496
+ */
497
+ readonly mounts?: ReadonlyArray<{
498
+ readonly path: string;
499
+ readonly app: {
500
+ fetch(request: Request): Response | Promise<Response>;
501
+ };
502
+ readonly stripPrefix?: boolean;
503
+ }>;
350
504
  /** Secret for **draft / preview mode** (see `enableDraft`). When set, a request carrying a valid
351
505
  * signed `__nifra_draft` cookie gets `ctx.draft === true` in loaders/actions (else always `false`).
352
506
  * Pair with `withISR({ draftSecret })` so editors bypass the cache. Omit to disable draft mode. */
353
507
  readonly draftSecret?: string;
354
- /** Per-route chunk URLs (`buildClient`'s `BuildManifest.routes`) — `routeId → [layout chunks…, own
508
+ /** Per-route chunk URLs (`buildClient`'s `BuildManifest.routes`) - `routeId → [layout chunks…, own
355
509
  * chunk]`. When present, each page `modulepreload`s its matched route's chunks alongside the entry,
356
510
  * so the route code downloads in parallel (no entry→route-chunk waterfall). Omit ⇒ entry-only. */
357
511
  readonly routePreload?: Readonly<Record<string, readonly string[]>>;
358
- /** The app's bundled stylesheet URLs (`buildClient`'s `BuildManifest.css`) — the aggregate, injected
512
+ /** The app's bundled stylesheet URLs (`buildClient`'s `BuildManifest.css`) - the aggregate, injected
359
513
  * as `<link rel="stylesheet">` in a page's `<head>`. Used as the fallback for any route absent from
360
514
  * {@link routeStyles}. Omit ⇒ no links (dev, where Vite injects CSS, or a CSS-free app). */
361
515
  readonly styles?: readonly string[];
362
- /** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) — `routeId → [chain CSS]`.
516
+ /** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) - `routeId → [chain CSS]`.
363
517
  * When a matched route has an entry here, only those (its layout chain + own CSS) are linked instead
364
518
  * of the aggregate `styles`, so a page ships only the CSS it uses. An empty array ⇒ no `<link>` (the
365
519
  * page imports no CSS). Routes absent here fall back to `styles`. Omit ⇒ always use `styles`. */
@@ -370,14 +524,14 @@ export interface CreateWebAppOptions {
370
524
  readonly prerenderedPaths?: readonly string[];
371
525
  /** SSG: per dynamic route pattern, its `getStaticPaths` `fallback` (from `enumerateStaticRoutes` or
372
526
  * the build's `prerendered.json`). A route mapped to `"404"` rejects any path NOT in
373
- * `prerenderedPaths` with the 404 page — the unlisted path simply doesn't exist. `"ssr"` (the
527
+ * `prerenderedPaths` with the 404 page - the unlisted path simply doesn't exist. `"ssr"` (the
374
528
  * default for unmapped routes) renders unlisted paths on-demand. */
375
529
  readonly staticFallbacks?: Readonly<Record<string, "ssr" | "404">>;
376
- /** Observe every loader/action failure — for error-reporting plugins (Sentry-style). Called for
530
+ /** Observe every loader/action failure - for error-reporting plugins (Sentry-style). Called for
377
531
  * real throws (not control-flow `Response`s like `redirect`), **before** the nearest `_error`
378
- * boundary renders / a soft-nav 500 / a rethrow — so it sees errors that the boundary would
532
+ * boundary renders / a soft-nav 500 / a rethrow - so it sees errors that the boundary would
379
533
  * otherwise hide. Observation only; its own throws are swallowed so a faulty reporter can't break
380
- * rendering. (`beforeLoader` is intentionally omitted — the core HTTP hooks already cover
534
+ * rendering. (`beforeLoader` is intentionally omitted - the core HTTP hooks already cover
381
535
  * pre-request work.) */
382
536
  readonly onLoaderError?: (error: unknown, ctx: {
383
537
  readonly request: Request;
@@ -388,21 +542,29 @@ export interface CreateWebAppOptions {
388
542
  /**
389
543
  * Build a nifra app from a route manifest: every route SSRs its layout chain via `renderPage`,
390
544
  * and a wildcard catch-all renders `_404` (or a plain 404). Reuses @nifrajs/core's router +
391
- * lifecycle, so matching, params, and precedence are battle-tested. fs-free — feed it a
545
+ * lifecycle, so matching, params, and precedence are battle-tested. fs-free - feed it a
392
546
  * manifest from `discoverRoutes` (`@nifrajs/web/fs`) at startup, so the served app stays portable.
393
547
  *
394
- * **Typed platform bindings.** Pass `Env` — `createWebApp<Env>({ … })` — to declare the app's Workers
548
+ * **Typed platform bindings.** Pass `Env` - `createWebApp<Env>({ … })` - to declare the app's Workers
395
549
  * bindings ONCE. It seeds the returned `Server`'s context with `{ env: Env }` (exactly as the backend's
396
550
  * `server<Env>()` does), so `app.fetch(req, { env })` / `toFetchHandler(app)` type-check the `env`
397
- * argument against the declared shape — no per-binding cast at the edge entry. Per-route loaders/actions
551
+ * argument against the declared shape - no per-binding cast at the edge entry. Per-route loaders/actions
398
552
  * stay typed independently of this call: annotate them with `@nifrajs/client`'s `LoaderArgs<Api, Env>`
399
- * (same `Env`) so `ctx.env.MY_KV` is typed there too. Omit the parameter and `Env` is `unknown` — the
553
+ * (same `Env`) so `ctx.env.MY_KV` is typed there too. Omit the parameter and `Env` is `unknown` - the
400
554
  * secure default; validate at the trust boundary before use.
401
555
  */
402
556
  export declare function createWebApp<Env = unknown>(options: CreateWebAppOptions): ReturnType<typeof server<Env>>;
403
557
  export interface GenerateClientEntryOptions {
404
- /** Module specifier for the adapter's client runtime (exports `mountRouter`), e.g.
405
- * `"@nifrajs/web-solid/client"`. */
558
+ /**
559
+ * Module specifier for the adapter's client runtime, e.g. `"@nifrajs/web-solid/client"`.
560
+ *
561
+ * **Contract:** the module MUST export `mountRouter({ router, routes, container })`. The generated
562
+ * bootstrap imports the specifier and calls it - so a self-executing entry that mounts on import
563
+ * builds cleanly and then does nothing, which is the trap this contract exists to name. The type is
564
+ * a bare `string` because a specifier is resolved by the bundler, not the type system, so the
565
+ * requirement is enforced by the bootstrap instead: it throws immediately, naming this module and
566
+ * the missing export, rather than failing later as `mountRouter is not a function`.
567
+ */
406
568
  readonly clientModule: string;
407
569
  /** Turn a route/layout source file (relative to the routes dir) into an import specifier. */
408
570
  readonly resolve: (file: string) => string;
@@ -418,36 +580,66 @@ export interface GenerateClientEntryOptions {
418
580
  * bundle it with `buildClient` / `Bun.build` (+ the adapter's transform).
419
581
  */
420
582
  export declare function generateClientEntry(manifest: Manifest, options: GenerateClientEntryOptions): string;
583
+ export interface GenerateRouteSearchTypesOptions {
584
+ /** Turn a route source file into a module specifier for a `typeof import(...)` type - the same contract
585
+ * as `generateClientEntry`'s `resolve`, but WITHOUT a file extension (a `.d.ts` imports a module, not a
586
+ * path). Relative to wherever the emitted `.d.ts` is written. */
587
+ readonly resolve: (file: string) => string;
588
+ /** The module whose `RouteSearch` interface is augmented (default `"@nifrajs/web"`). Every adapter's
589
+ * `NavigateTarget` reads `@nifrajs/web`'s `RouteSearch`, so the default types `navigate` on all of them. */
590
+ readonly module?: string;
591
+ }
592
+ /**
593
+ * Codegen: emit a `.d.ts` that types cross-route `navigate({ to, search })` against each route's
594
+ * `searchSchema`. It augments the {@link RouteSearch} map with `"<pattern>": SearchOf<typeof import(...)>`
595
+ * for every **static** route (one with no `:param`/`*` segment - a concrete `to` a caller can pass; a
596
+ * dynamic route's concrete path can't match its pattern key, so those keep the loose object form). Written
597
+ * by `nifra sync-routes` next to the route tree and included in the app's `tsconfig`; because it is
598
+ * regenerated from the route files, a `navigate` whose `search` no longer matches the route's schema
599
+ * becomes a `tsc` error - the same drift guarantee as nifra's typed client, no hand-maintained map.
600
+ */
601
+ export declare function generateRouteSearchTypes(manifest: Manifest, options: GenerateRouteSearchTypesOptions): string;
421
602
  export interface GenerateServerManifestOptions {
422
- /** Turn a route/layout source file (relative to the routes dir) into an import specifier —
603
+ /** Turn a route/layout source file (relative to the routes dir) into an import specifier -
423
604
  * same contract as `generateClientEntry`'s `resolve`. */
424
605
  readonly resolve: (file: string) => string;
425
606
  /** The content-hashed client entry URL (from `buildClient`'s manifest), **baked** into the emitted
426
- * module — a disk-less worker can't read `manifest.json` at runtime. */
607
+ * module - a disk-less worker can't read `manifest.json` at runtime. */
427
608
  readonly clientEntry: string;
428
- /** The app's aggregate stylesheet URLs (`buildClient`'s `BuildManifest.css`) — baked as
609
+ /** The app's aggregate stylesheet URLs (`buildClient`'s `BuildManifest.css`) - baked as
429
610
  * `export const styles` so the server entry can hand them to `createWebApp` (→ `<link rel="stylesheet">`
430
611
  * in `<head>`). Without this the built SSR page ships no CSS link and renders unstyled. */
431
612
  readonly styles?: readonly string[] | undefined;
432
- /** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) — baked as
613
+ /** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) - baked as
433
614
  * `export const routeStyles` so a page links only the CSS its route chain uses. */
434
615
  readonly routeStyles?: Readonly<Record<string, readonly string[]>> | undefined;
435
616
  /** Emit **lazy** per-route loaders (`() => import("./routes/x")`, a static specifier) instead of
436
- * eager `import * as`, so a bundler with code-splitting emits one chunk per route — loaded on the
617
+ * eager `import * as`, so a bundler with code-splitting emits one chunk per route - loaded on the
437
618
  * first request to it, not all at boot (smaller cold-start parse). Default `false` (eager). Both
438
619
  * modes are fs-free with statically-analyzable specifiers; only the *when* differs. */
439
620
  readonly lazy?: boolean;
440
621
  }
441
622
  /**
442
623
  * Codegen: emit a **server manifest** module (as source) for disk-less edge runtimes (Cloudflare
443
- * Workers, …) — and, with a `target`, any portable server bundle. `discoverRoutes` scans `node:fs`
444
- * and dynamic-imports each route by a *runtime* path — neither exists on workerd. This instead emits
445
- * **statically-analyzable** imports of every route/layout/`_error`/`_404` (so the bundler includes them) and
446
- * rebuilds the manifest with `buildManifest` — the SAME pure logic `discoverRoutes` feeds, so patterns
624
+ * Workers, …) - and, with a `target`, any portable server bundle. `discoverRoutes` scans `node:fs`
625
+ * and dynamic-imports each route by a *runtime* path - neither exists on workerd. This instead emits
626
+ * **statically-analyzable** imports of every route/layout/`_error`/terminal status page (so the bundler includes them) and
627
+ * rebuilds the manifest with `buildManifest` - the SAME pure logic `discoverRoutes` feeds, so patterns
447
628
  * + layout chains are identical. Eager (`import * as`) by default; `lazy` emits `() => import(...)` so
448
629
  * a code-splitting bundler chunks per route. The emitted module exports `manifest` (consumed by
449
630
  * `createWebApp`, unchanged) + `clientEntry` (baked). Write it to a file and bundle it into the worker
450
631
  * entry (see `buildServer` in `@nifrajs/web/build`).
451
632
  */
452
633
  export declare function generateServerManifest(manifest: Manifest, options: GenerateServerManifestOptions): string;
634
+ /**
635
+ * The two file-name conventions that decide what never reaches a browser: a `*.server` module is
636
+ * EMPTIED in the client build, a `*.fn` module is REPLACED with client stubs.
637
+ *
638
+ * Exported because more than the bundlers need them. `nifra dev --bun` cannot transform (Bun's dev
639
+ * bundler takes no plugins) so it must REFUSE instead, and a refusal driven by its own hand-written
640
+ * glob drifts from the transform - which is how `.fn.mts` came to be stubbed by both build pipelines
641
+ * and waved through by the guard that exists to stop it leaking. Anything deciding "is this module
642
+ * server-only" should import the matcher rather than re-encode it.
643
+ */
644
+ export { SERVER_FN_MODULE, SERVER_ONLY_MODULE } from "./internal/server-boundary.js";
453
645
  //# sourceMappingURL=index.d.ts.map