@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
@@ -1,15 +1,16 @@
1
1
  /**
2
- * Route manifest — the fs-free heart of file-based routing. Maps route file paths to nifra
2
+ * Route manifest - the fs-free heart of file-based routing. Maps route file paths to nifra
3
3
  * router patterns + their nested layout chain. `discoverRoutes` (in `@nifrajs/web/fs`) scans
4
4
  * the filesystem and feeds `buildManifest`; everything here is pure logic, so it stays
5
5
  * portable (no fs, no DOM) and fully unit-testable. Edge deploys pre-build the manifest.
6
6
  */
7
+ import type { StandardSchemaV1 } from "@nifrajs/core/server";
7
8
  /** Context passed to a route `loader`. The `api` + `env` are injected by `createWebApp` and typed
8
9
  * per-route via `@nifrajs/client`'s `LoaderArgs<Api, Env>` (here they are opaque to the agnostic core). */
9
10
  export interface LoaderContext {
10
11
  readonly params: Record<string, string>;
11
12
  readonly request: Request;
12
- /** Alias of {@link request} — mirrors a route handler's `c.req` so the same name works in both. */
13
+ /** Alias of {@link request} - mirrors a route handler's `c.req` so the same name works in both. */
13
14
  readonly req: Request;
14
15
  readonly api: unknown;
15
16
  /** Platform bindings forwarded from the request `c.env` (Workers env/KV/D1). Opaque here. */
@@ -17,24 +18,28 @@ export interface LoaderContext {
17
18
  /** `true` when the request carries a valid draft/preview cookie (only when `createWebApp` is given a
18
19
  * `draftSecret`; otherwise always `false`). Branch on it to load unpublished content for editors. */
19
20
  readonly draft: boolean;
21
+ /** The URL search params, validated against the route's `searchSchema` (a Standard Schema) when it
22
+ * declares one - failing closed to the schema's defaults - else the raw parsed query. Typed per-route
23
+ * via `@nifrajs/client`'s `LoaderArgs<Api, Env, Search>`. */
24
+ readonly search: Record<string, unknown>;
20
25
  }
21
26
  /** A route's optional data loader: params/request in, data out. */
22
27
  export type Loader = (ctx: LoaderContext) => unknown | Promise<unknown>;
23
28
  /**
24
29
  * A route's optional mutation, run on POST. Shares the loader context (params/request/api);
25
- * read the form/JSON body off `request`. Returns either a `Response` (e.g. a redirect —
30
+ * read the form/JSON body off `request`. Returns either a `Response` (e.g. a redirect -
26
31
  * passed straight through) or data, surfaced to the page component as `actionData`.
27
32
  */
28
33
  export type Action = (ctx: LoaderContext) => unknown | Promise<unknown>;
29
34
  /**
30
35
  * One `<link>` tag's attributes for a route/layout's `meta.link`. The common HTML `<link>` attributes
31
- * are spelled out and **optional** so a typed partial like `{ rel, href, hreflang }` is assignable —
36
+ * are spelled out and **optional** so a typed partial like `{ rel, href, hreflang }` is assignable -
32
37
  * the previous `Record<string, string>` required *every* value to be a present string, which rejected
33
- * exactly that idiomatic shape (the bug this fixes). The index signature keeps custom/`data-*` attrs
34
- * (and any future standard attr) passing without a cast; `boolean` covers boolean attributes like
35
- * `disabled` (rendered as a bare attribute when `true`, omitted when `false`), and `undefined` lets a
36
- * caller spread in a conditionally-absent attribute. Attribute *names* are still shape-validated and
37
- * values HTML-escaped at render — a widened type never widens the injection surface.
38
+ * exactly that idiomatic shape (the bug this fixes). Standard attributes are explicit and the template
39
+ * index signature admits inert `data-*` metadata without opening executable `on*` attributes. `boolean`
40
+ * covers `disabled` (rendered bare when `true`, omitted when `false`), and `undefined` lets a caller
41
+ * spread in a conditionally absent attribute. SSR and soft navigation apply one runtime allowlist too,
42
+ * so a cast or untyped route cannot widen the injection surface.
38
43
  */
39
44
  export interface LinkDescriptor {
40
45
  readonly rel?: string;
@@ -42,6 +47,7 @@ export interface LinkDescriptor {
42
47
  readonly hreflang?: string;
43
48
  readonly crossorigin?: string;
44
49
  readonly media?: string;
50
+ readonly nonce?: string;
45
51
  readonly sizes?: string;
46
52
  readonly type?: string;
47
53
  readonly as?: string;
@@ -53,40 +59,79 @@ export interface LinkDescriptor {
53
59
  readonly imagesizes?: string;
54
60
  readonly color?: string;
55
61
  readonly disabled?: boolean;
56
- readonly [attr: string]: string | boolean | undefined;
62
+ readonly [attr: `data-${string}`]: string | undefined;
57
63
  }
58
- /** One `<script>` element a route contributes to `<head>` — for structured data (JSON-LD) and other
64
+ /** One managed `<meta>` tag. Standard attributes and inert `data-*` metadata only. */
65
+ export interface MetaDescriptor {
66
+ readonly charset?: string;
67
+ readonly content?: string;
68
+ readonly "http-equiv"?: string;
69
+ readonly itemprop?: string;
70
+ readonly media?: string;
71
+ readonly name?: string;
72
+ readonly property?: string;
73
+ readonly scheme?: string;
74
+ readonly [attr: `data-${string}`]: string | undefined;
75
+ }
76
+ /** One `<script>` element a route contributes to `<head>` - for structured data (JSON-LD) and other
59
77
  * inert, non-executable head scripts. The `content` is the script body; `type` defaults to
60
78
  * `"application/ld+json"` (the common case). The renderer escapes `content` against an HTML breakout
61
- * (`</`, `<!--`, `]]>`) — see `escapeScriptContent` — so a JSON-LD payload can never close the
79
+ * (`</`, `<!--`, `]]>`) - see `escapeScriptContent` - so a JSON-LD payload can never close the
62
80
  * `<script>` element early. `content` is **JSON/text, never raw HTML**: this slot is not an XSS escape
63
81
  * hatch for arbitrary markup. */
82
+ export type InertScriptType = "application/ld+json" | "application/json";
64
83
  export interface ScriptDescriptor {
65
84
  /** The script's `type` attribute. Default `"application/ld+json"`. */
66
- readonly type?: string;
85
+ readonly type?: InertScriptType;
67
86
  /** The script body (e.g. a `JSON.stringify`'d JSON-LD object). Escaped for safe `<script>` embedding. */
68
87
  readonly content: string;
69
88
  }
89
+ /** Explicit escape hatch for executable inline code. A CSP nonce is mandatory. */
90
+ export interface UnsafeScriptDescriptor {
91
+ readonly unsafe: true;
92
+ readonly type: "module" | "text/javascript";
93
+ readonly nonce: string;
94
+ readonly content: string;
95
+ }
70
96
  /**
71
- * The document head a route contributes — title + `<meta>`/`<link>`/`<script>` tag sets. Returned by a
97
+ * The document head a route contributes - title + `<meta>`/`<link>`/`<script>` tag sets. Returned by a
72
98
  * route/layout `meta` (statically, or from a {@link MetaArgs} function). Every value is serialized into
73
- * managed (`data-nifra`) head tags: attribute *names* are shape-validated and *values* HTML-escaped at
74
- * render (see `tagAttrs`/`headTags` in `@nifrajs/web`), and `script[].content` is breakout-escaped — so
75
- * loader-derived strings (LLM-authored `og:*`, user content) are XSS-safe in the head by construction.
99
+ * managed (`data-nifra`) head tags: tag-specific attribute allowlists reject event handlers and active
100
+ * URL/refresh contexts, values are HTML-escaped at render, and `script[].content` is breakout-escaped -
101
+ * so loader-derived strings (LLM-authored `og:*`, user content) are XSS-safe by construction.
76
102
  * Layout-chain heads merge with the page's via `mergeHeads` (arrays concat outermost→page; `title` is
77
103
  * nearest-wins). Build `og:*`/`twitter:*` with `openGraph(...)`, canonical with `canonical(...)`, and
78
104
  * JSON-LD with `jsonLd(...)` (all from `@nifrajs/web`) rather than hand-writing the records.
79
105
  */
80
106
  export interface Meta {
81
107
  readonly title?: string;
82
- readonly meta?: ReadonlyArray<Record<string, string>>;
108
+ readonly meta?: readonly MetaDescriptor[];
83
109
  readonly link?: readonly LinkDescriptor[];
84
110
  /** Inert head `<script>`s (JSON-LD structured data, etc.). See {@link ScriptDescriptor}. */
85
111
  readonly script?: readonly ScriptDescriptor[];
112
+ /** Executable inline scripts. Prefer external modules; construct only with `unsafeInlineScript()`. */
113
+ readonly unsafeScript?: readonly UnsafeScriptDescriptor[];
114
+ /**
115
+ * The document language - `<html lang="...">`. Nearest-wins like `title`, so a localized route can
116
+ * override a layout's default. Defaults to `"en"` when no head in the chain sets it.
117
+ *
118
+ * This is the ONLY way to set it: the shell's `<html>` is framework-owned, so a multilingual app
119
+ * otherwise serves every URL as `lang="en"` - which tells a screen reader to pronounce every locale
120
+ * with an English voice, and suppresses the browser's translation offer.
121
+ */
122
+ readonly lang?: string;
123
+ /**
124
+ * The document writing direction - `<html dir="...">`. Nearest-wins like `title`. Omitted from the
125
+ * shell when unset, which is HTML's `ltr` default, so an LTR app's output is byte-identical.
126
+ *
127
+ * Required for Arabic/Hebrew/Urdu/Persian: without it the browser lays the page out left-to-right
128
+ * and mis-orders trailing punctuation, so the text renders wrong rather than merely unstyled.
129
+ */
130
+ readonly dir?: "ltr" | "rtl" | "auto";
86
131
  }
87
132
  /**
88
133
  * Args for a route's `meta` function: the loader's `data` + the route `params` + the request `origin`.
89
- * `meta()` runs in BOTH SSR and client navigation, so it has **no `request`/`process.env`/server access** —
134
+ * `meta()` runs in BOTH SSR and client navigation, so it has **no `request`/`process.env`/server access** -
90
135
  * `origin` is the only server-resolved fact it gets (so you needn't thread `siteUrl` through loader data
91
136
  * for absolute `og:url`/`canonical`/`og:image` URLs). See {@link origin}.
92
137
  */
@@ -94,10 +139,10 @@ export interface MetaArgs<Data = unknown> {
94
139
  readonly data: Data;
95
140
  readonly params: Record<string, string>;
96
141
  /**
97
- * The site origin — scheme + host (+ port), e.g. `"https://news.example.com"`, **with no trailing
142
+ * The site origin - scheme + host (+ port), e.g. `"https://news.example.com"`, **with no trailing
98
143
  * slash**. The single piece of server/env knowledge `meta()` otherwise can't see: it runs in BOTH
99
144
  * SSR and client navigation, so it has no `request`/`process.env`. The framework resolves it from the
100
- * request URL during SSR and from `location.origin` on client nav — and they match, so an absolute
145
+ * request URL during SSR and from `location.origin` on client nav - and they match, so an absolute
101
146
  * `og:url`/`canonical`/`og:image` built from it never drifts between the server-rendered `<head>` and
102
147
  * a soft-nav. Use it for absolute URLs (`origin + "/posts/" + slug`) instead of threading `siteUrl`
103
148
  * through loader data. Empty string (`""`) when the origin is unknown (e.g. a hand-built test render
@@ -106,7 +151,7 @@ export interface MetaArgs<Data = unknown> {
106
151
  readonly origin: string;
107
152
  }
108
153
  /** A route's `meta`: a static {@link Meta}, or a function of the loader data + params + the request
109
- * origin ({@link MetaArgs}). Use the `origin` arg for absolute `canonical`/`og:url`/`og:image` URLs —
154
+ * origin ({@link MetaArgs}). Use the `origin` arg for absolute `canonical`/`og:url`/`og:image` URLs -
110
155
  * it's resolved server-side from the request and matches the client's `location.origin`. */
111
156
  export type MetaInput = Meta | ((args: MetaArgs) => Meta);
112
157
  /** One concrete parameterization of a dynamic route, returned by {@link GetStaticPaths}. */
@@ -119,18 +164,28 @@ export interface StaticPaths {
119
164
  readonly paths: readonly StaticPath[];
120
165
  /**
121
166
  * How a path NOT in `paths` is handled. `"ssr"` (default) → rendered on-demand by the worker
122
- * (the natural hybrid behavior — an unlisted path simply isn't a static file); `"404"` → only the
167
+ * (the natural hybrid behavior - an unlisted path simply isn't a static file); `"404"` → only the
123
168
  * listed paths exist. Recorded by `prerenderRoutes` for the deploy layer.
124
169
  */
125
170
  readonly fallback?: "ssr" | "404";
126
171
  }
127
172
  /** A dynamic route's build-time param enumeration (the SSG equivalent of "which pages exist"). */
128
173
  export type GetStaticPaths = () => StaticPaths | Promise<StaticPaths>;
129
- /** A route module — the default component + optional loader / action / meta. */
174
+ /** A route module - the default component + optional loader / action / meta. */
130
175
  export interface RouteModule {
131
176
  readonly default: unknown;
132
177
  readonly loader?: Loader;
133
178
  readonly action?: Action;
179
+ /** A Standard Schema validating this route's URL search params. When present, `ctx.search` is parsed +
180
+ * validated against it (failing closed to the schema's defaults on invalid input); type it into the
181
+ * loader with `LoaderArgs<Api, Env, typeof searchSchema>`. */
182
+ readonly searchSchema?: StandardSchemaV1<unknown, Record<string, unknown>>;
183
+ /** Search keys that are purely client-side UI (`"tab"`, a client-side `"sort"`, `"modal"`) and do NOT
184
+ * affect this route's loader. When a client navigation stays on the same route + pathname and changes
185
+ * ONLY these keys, the router updates the URL (so `useSearch` re-renders) WITHOUT re-running the loader.
186
+ * Any other key change revalidates as usual - so omitting a data-affecting key here can never serve
187
+ * stale data, it only forgoes the optimization. Client-only: the server always renders fresh. */
188
+ readonly searchClientKeys?: readonly string[];
134
189
  readonly meta?: MetaInput;
135
190
  /**
136
191
  * Opt this route out of nifra's full-document client hydration. The server still renders the full
@@ -142,12 +197,12 @@ export interface RouteModule {
142
197
  /**
143
198
  * Opt a **static** route (no `:param`/`*`) into build-time prerendering (SSG): `prerenderRoutes`
144
199
  * (from `@nifrajs/web/build`) renders it to a static `index.html` at build. The loader runs at build
145
- * with the in-process `api` (build-safe data only — no per-request cookies/secrets); `defer()` on a
200
+ * with the in-process `api` (build-safe data only - no per-request cookies/secrets); `defer()` on a
146
201
  * prerendered route resolves at build. For **dynamic** routes use {@link getStaticPaths} instead.
147
202
  */
148
203
  readonly prerender?: boolean;
149
204
  /**
150
- * Enumerate the concrete params to prerender for a **dynamic** (`:param`) route — the SSG path list
205
+ * Enumerate the concrete params to prerender for a **dynamic** (`:param`) route - the SSG path list
151
206
  * (blogs/docs/etc.). Runs at build; `prerenderRoutes` renders one `index.html` per returned path.
152
207
  */
153
208
  readonly getStaticPaths?: GetStaticPaths;
@@ -159,7 +214,7 @@ export interface RouteModule {
159
214
  readonly revalidate?: number;
160
215
  /**
161
216
  * No-framework island bundles (`@nifrajs/web/islands`) to load on this route, as `<script
162
- * type="module">` in the document tail. Loaded **regardless of `hydrate`** — pair with
217
+ * type="module">` in the document tail. Loaded **regardless of `hydrate`** - pair with
163
218
  * `export const hydrate = false` for a static page that ships zero framework JS and mounts
164
219
  * interactivity through `<Island>` markers + `mountIslands` enhancers instead.
165
220
  */
@@ -168,7 +223,7 @@ export interface RouteModule {
168
223
  /** A layout (or `_404`/`_error`) entry: its source file (for client codegen) + a lazy loader.
169
224
  *
170
225
  * A `_layout.tsx` may export `meta` (static {@link Meta} or a function of the loader data + params,
171
- * same shape as a route's) to contribute sitewide `<head>` tags — `hreflang`/`preconnect`/etc. that
226
+ * same shape as a route's) to contribute sitewide `<head>` tags - `hreflang`/`preconnect`/etc. that
172
227
  * belong on every page under the layout. The layout chain's heads merge with the page's: arrays
173
228
  * (`meta`/`link`) concatenate outermost→innermost→page; scalars (`title`) are nearest-wins (the page
174
229
  * overrides an inner layout, which overrides an outer one). See `mergeHeads` in `@nifrajs/web`. */
@@ -184,6 +239,16 @@ export interface RouteEntry {
184
239
  readonly id: string;
185
240
  readonly pattern: string;
186
241
  readonly layoutIds: readonly string[];
242
+ /**
243
+ * Param names each layout in {@link layoutIds} owns, aligned by index.
244
+ *
245
+ * A layout wraps a URL prefix, so these are the params it may read and the ones whose change should
246
+ * re-run its loader - everything deeper belongs to routes beneath it. Derived per (route, layout)
247
+ * pair rather than per layout, because one layout can own different params on different expanded
248
+ * patterns: `[[lang]]/docs/_layout` owns nothing on `/docs/:slug` and `{lang}` on `/:lang/docs/:slug`.
249
+ * Optional only so hand-built test manifests may omit it.
250
+ */
251
+ readonly layoutParams?: ReadonlyArray<readonly string[]>;
187
252
  /** `_error` boundary ids in this route's ancestor chain (outermost → innermost). The last is the
188
253
  * **nearest** boundary, rendered when the route's loader throws. Always set by `buildManifest`
189
254
  * (optional only so hand-built test manifests may omit it); absent/empty ⇒ no boundary (error 500s). */
@@ -199,25 +264,41 @@ export interface Manifest {
199
264
  * `buildManifest` (optional only so hand-built test manifests may omit it). */
200
265
  readonly errors?: Readonly<Record<string, LayoutEntry>>;
201
266
  readonly notFound?: LayoutEntry;
267
+ /** Per-status terminal pages from `_<status>.tsx` at the routes root (`_410`, `_451`, …), keyed by
268
+ * the status as a string. Rendered by a loader's `gone()` / `statusPage(n)`; a status with no page
269
+ * falls back to `_404`, and then to plain text. `_404` itself stays on {@link notFound} - it is
270
+ * reached by unmatched paths too, which these are not. */
271
+ readonly statusPages?: Readonly<Record<string, LayoutEntry>>;
202
272
  }
203
- /**
204
- * Derive **every** nifra router pattern a route file maps to (relative to the routes dir):
205
- * `index` → the parent path, `[id]` → `:id`, `[...slug]` → `*slug` (catch-all, captures the rest of
206
- * the path into one param), `(group)` folders are dropped from the URL (organization only), and an
207
- * optional `[[lang]]` expands the set — once with the segment present (`:lang`) and once absent. A file
208
- * with no optionals yields exactly one pattern. Throws on an invalid param or a catch-all that isn't
209
- * the last segment.
210
- */
211
273
  export declare function filePathToPatterns(file: string): string[];
274
+ /** One URL pattern a route file expands to, plus the scope information layouts need. */
275
+ export interface FileRoutePattern {
276
+ readonly pattern: string;
277
+ /**
278
+ * URL segments contributed by the first *k* path parts of the file, indexed by *k*.
279
+ *
280
+ * This is what makes a layout's scope derivable without turning layouts into router nodes. A layout
281
+ * lives at a directory; the segments that directory contributes are exactly the prefix of the URL it
282
+ * wraps, so `depths[partsIn(layoutDir)]` is how many leading pattern segments it owns - and the
283
+ * params inside that prefix are the ones it may read.
284
+ *
285
+ * Per EXPANDED pattern, not per file, because the two diverge: `[[lang]]/docs/_layout` owns nothing
286
+ * on `/docs/:slug` and `{lang}` on `/:lang/docs/:slug`. A route group contributes no segment, so
287
+ * `(marketing)/_layout` owns nothing at all.
288
+ */
289
+ readonly depths: readonly number[];
290
+ }
291
+ /** Expand a route file into its URL patterns, each carrying per-directory segment counts. */
292
+ export declare function filePathToRoutes(file: string): FileRoutePattern[];
212
293
  /**
213
- * The **canonical** single pattern for a route file — all optional segments present. A file with no
294
+ * The **canonical** single pattern for a route file - all optional segments present. A file with no
214
295
  * optionals yields its one pattern. Use {@link filePathToPatterns} to get every pattern (optionals
215
296
  * expand the set).
216
297
  */
217
298
  export declare function filePathToPattern(file: string): string;
218
299
  /**
219
300
  * Build a manifest from route file paths (relative to the routes dir) + an `importer` that
220
- * turns a path into a lazy module loader. Pure — no fs. Throws at boot (the loud-and-early
301
+ * turns a path into a lazy module loader. Pure - no fs. Throws at boot (the loud-and-early
221
302
  * RouteConfigError ethos) on duplicate patterns. `_layout`/`_404`/`_error` files are special; other
222
303
  * `_`-prefixed files are ignored (private/colocated, never routed).
223
304
  */
@@ -235,14 +316,14 @@ export declare function fillRoutePattern(pattern: string, params: Record<string,
235
316
  /** The static-routing facts a server needs from the route modules: which concrete paths are
236
317
  * prerendered, plus each dynamic route's `getStaticPaths` fallback policy. */
237
318
  export interface StaticRoutes {
238
- /** Concrete prerendered paths — static `prerender` routes + each `getStaticPaths` entry. */
319
+ /** Concrete prerendered paths - static `prerender` routes + each `getStaticPaths` entry. */
239
320
  readonly paths: string[];
240
321
  /** Per dynamic route pattern, its `getStaticPaths` `fallback` (`"ssr"` default). `createWebApp` uses
241
322
  * `"404"` to reject an unlisted path under that route at runtime (the path simply doesn't exist). */
242
323
  readonly fallbacks: Record<string, "ssr" | "404">;
243
324
  }
244
325
  /**
245
- * Enumerate the static-routing facts `prerenderRoutes` would produce — static routes opted in via
326
+ * Enumerate the static-routing facts `prerenderRoutes` would produce - static routes opted in via
246
327
  * `export const prerender = true`, each `getStaticPaths` entry of a dynamic route, and each dynamic
247
328
  * route's `fallback` policy. Pure (no rendering), so a server can compute what to hand `createWebApp`
248
329
  * (the prerendered set for the client's static-`_data.json` soft-nav + the fallback map for
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
package/dist/manifest.js CHANGED
@@ -1,12 +1,6 @@
1
- /**
2
- * Route manifest — the fs-free heart of file-based routing. Maps route file paths to nifra
3
- * router patterns + their nested layout chain. `discoverRoutes` (in `@nifrajs/web/fs`) scans
4
- * the filesystem and feeds `buildManifest`; everything here is pure logic, so it stays
5
- * portable (no fs, no DOM) and fully unit-testable. Edge deploys pre-build the manifest.
6
- */
7
1
  // `.svelte` and `.vue` routes are supported too: their `default` export is the component and
8
2
  // `loader`/`action`/`meta` come from a module-level script block (Svelte `<script module>`, Vue's plain
9
- // `<script>`) as named ESM exports — the same RouteModule shape as `.tsx`. Both compile via their
3
+ // `<script>`) as named ESM exports - the same RouteModule shape as `.tsx`. Both compile via their
10
4
  // package's Bun plugin (`@nifrajs/web-svelte/plugin`, `@nifrajs/web-vue/plugin`).
11
5
  const ROUTE_EXT = /\.(tsx|jsx|svelte|vue|mdx)$/;
12
6
  const PARAM = /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/;
@@ -15,8 +9,12 @@ const CATCH_ALL = /^\[\.\.\.([A-Za-z_][A-Za-z0-9_]*)\]$/;
15
9
  // two patterns (`:lang` present / absent), so `[[lang]]/about` serves both `/about` and `/en/about`.
16
10
  const OPTIONAL = /^\[\[([A-Za-z_][A-Za-z0-9_]*)\]\]$/;
17
11
  // A route group: a `(name)` folder organizes routes (and can hold its own `_layout`) without
18
- // contributing a URL segment — mirrors Next/Remix. Requires content between the parens.
12
+ // contributing a URL segment - mirrors Next/Remix. Requires content between the parens.
19
13
  const GROUP = /^\(.+\)$/;
14
+ // A terminal status page: `_410.tsx`, `_451.tsx`. `_404` is matched before this and stays its own
15
+ // thing. Restricted to 3 digits so `_401k` or `_4` is treated as an ordinary underscore-prefixed
16
+ // file (ignored) rather than silently becoming a status page.
17
+ const STATUS_PAGE = /^_[1-5][0-9][0-9]$/;
20
18
  const stripExt = (file) => file.replace(ROUTE_EXT, "");
21
19
  const baseName = (file) => file.slice(file.lastIndexOf("/") + 1);
22
20
  const dirOf = (file) => file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
@@ -26,20 +24,92 @@ const errorIdFor = (dir) => (dir === "" ? "_error" : `${dir}/_error`);
26
24
  * Derive **every** nifra router pattern a route file maps to (relative to the routes dir):
27
25
  * `index` → the parent path, `[id]` → `:id`, `[...slug]` → `*slug` (catch-all, captures the rest of
28
26
  * the path into one param), `(group)` folders are dropped from the URL (organization only), and an
29
- * optional `[[lang]]` expands the set — once with the segment present (`:lang`) and once absent. A file
27
+ * optional `[[lang]]` expands the set - once with the segment present (`:lang`) and once absent. A file
30
28
  * with no optionals yields exactly one pattern. Throws on an invalid param or a catch-all that isn't
31
29
  * the last segment.
32
30
  */
31
+ /** Every `[…]` marker in a file segment, so a part-literal segment can be spliced into a pattern. */
32
+ const FILE_MARKER = /\[([^\]]*)\]/g;
33
+ const FILE_PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
34
+ /**
35
+ * Translate a part-literal-part-`[param]` file segment into its route pattern, or `undefined` when
36
+ * the segment is wholly literal or wholly a marker (both already handled by the caller).
37
+ *
38
+ * `[inKey].txt` becomes `:inKey.txt`. Needed because IndexNow-style verification files, per-locale
39
+ * sitemaps, and static-site URL parity (`post-[id].html`) all put a variable INSIDE a filename, and
40
+ * `stripExt` already reduces `[inKey].txt.tsx` to `[inKey].txt` - `robots.txt.tsx` proves literal
41
+ * dots survive into a segment, so this was only ever a parser gap.
42
+ *
43
+ * `[[optional]]` and `[...catchAll]` are **rejected** inside a mixed segment rather than given a
44
+ * meaning. There is no sensible absent form for `/[[locale]]-feed.xml` - `/-feed.xml` and dropping
45
+ * the segment are both surprising - and a catch-all captures the remaining path verbatim, which
46
+ * conflicts with a trailing literal. A clear rejection beats semantics nobody can predict.
47
+ */
48
+ function mixedFileSegment(seg, file) {
49
+ // A segment that is ENTIRELY one of the existing whole-segment forms belongs to the caller, not
50
+ // here. Checked against the real patterns rather than by comparing the first marker to the
51
+ // segment: `[[lang]]` does not equal its own first `[…]` match, so a naive comparison sends every
52
+ // optional segment down the mixed path and rejects it.
53
+ if (PARAM.test(seg) || OPTIONAL.test(seg) || CATCH_ALL.test(seg))
54
+ return undefined;
55
+ const markers = [...seg.matchAll(FILE_MARKER)];
56
+ if (markers.length === 0)
57
+ return undefined;
58
+ let pattern = "";
59
+ let cursor = 0;
60
+ for (const marker of markers) {
61
+ const inner = marker[1] ?? "";
62
+ if (inner.startsWith("...")) {
63
+ throw new Error(`[nifra/web] catch-all "[${inner}]" cannot be combined with literal text in "${file}" (segment "${seg}") - a catch-all captures the rest of the path, so a trailing literal can never match. Give it its own segment.`);
64
+ }
65
+ if (inner.startsWith("[")) {
66
+ throw new Error(`[nifra/web] optional "[${inner}]]" cannot be combined with literal text in "${file}" (segment "${seg}") - there is no sensible form for the segment when it is absent. Use a required [name], or give the optional its own segment.`);
67
+ }
68
+ if (!FILE_PARAM_NAME.test(inner)) {
69
+ throw new Error(`[nifra/web] invalid route param in "${file}": "[${inner}]" must be [name], [[name]], [...name], or a (group) folder`);
70
+ }
71
+ pattern += `${seg.slice(cursor, marker.index)}:${inner}`;
72
+ cursor = (marker.index ?? 0) + marker[0].length;
73
+ }
74
+ return pattern + seg.slice(cursor);
75
+ }
33
76
  export function filePathToPatterns(file) {
34
- // Each combo is a list of URL segments built so far; an optional segment doubles the set.
35
- let combos = [[]];
77
+ return filePathToRoutes(file).map((route) => route.pattern);
78
+ }
79
+ /** Expand a route file into its URL patterns, each carrying per-directory segment counts. */
80
+ export function filePathToRoutes(file) {
81
+ // Each combo is the URL segments built so far, plus the running per-path-part segment count.
82
+ let combos = [{ segs: [], depths: [0] }];
36
83
  const segments = stripExt(file).split("/");
84
+ // Record the running segment count after each path part, on every combo. Called once per part so
85
+ // `depths` stays index-aligned with the file's path parts even for parts that emit nothing.
86
+ const mark = () => {
87
+ for (const combo of combos)
88
+ combo.depths.push(combo.segs.length);
89
+ };
90
+ const push = (segment) => {
91
+ for (const combo of combos)
92
+ combo.segs.push(segment);
93
+ };
37
94
  for (let i = 0; i < segments.length; i++) {
38
95
  const seg = segments[i];
39
- if (seg === "index")
96
+ if (seg === "index") {
97
+ mark();
40
98
  continue;
41
- if (GROUP.test(seg))
42
- continue; // route group — no URL segment (its `_layout` still applies, keyed by dir)
99
+ }
100
+ if (GROUP.test(seg)) {
101
+ // Route group - no URL segment (its `_layout` still applies, keyed by dir).
102
+ mark();
103
+ continue;
104
+ }
105
+ // A segment that is part literal, part `[param]` - `[inKey].txt`, `post-[id].html`. Checked
106
+ // before the whole-segment forms below, which only handle a marker spanning the entire segment.
107
+ const mixed = mixedFileSegment(seg, file);
108
+ if (mixed !== undefined) {
109
+ push(mixed);
110
+ mark();
111
+ continue;
112
+ }
43
113
  if (seg.startsWith("[")) {
44
114
  const catchAll = CATCH_ALL.exec(seg);
45
115
  if (catchAll !== null) {
@@ -49,29 +119,35 @@ export function filePathToPatterns(file) {
49
119
  if (after.length > 0) {
50
120
  throw new Error(`[nifra/web] catch-all "${seg}" must be the last segment in "${file}" (found "${after.join("/")}" after it)`);
51
121
  }
52
- combos = combos.map((c) => [...c, `*${catchAll[1]}`]);
122
+ push(`*${catchAll[1]}`);
123
+ mark();
53
124
  continue;
54
125
  }
55
126
  const optional = OPTIONAL.exec(seg);
56
127
  if (optional !== null) {
57
128
  // Optional segment: keep each existing combo (absent) AND a copy with `:name` appended (present).
58
- combos = combos.flatMap((c) => [c, [...c, `:${optional[1]}`]]);
129
+ combos = combos.flatMap((c) => [
130
+ { segs: c.segs, depths: c.depths },
131
+ { segs: [...c.segs, `:${optional[1]}`], depths: [...c.depths] },
132
+ ]);
133
+ mark();
59
134
  continue;
60
135
  }
61
136
  const match = PARAM.exec(seg);
62
137
  if (match === null) {
63
138
  throw new Error(`[nifra/web] invalid route param in "${file}": "${seg}" must be [name], [[name]], [...name], or a (group) folder`);
64
139
  }
65
- combos = combos.map((c) => [...c, `:${match[1]}`]);
140
+ push(`:${match[1]}`);
66
141
  }
67
142
  else {
68
- combos = combos.map((c) => [...c, seg]);
143
+ push(seg);
69
144
  }
145
+ mark();
70
146
  }
71
- return combos.map((c) => `/${c.join("/")}`);
147
+ return combos.map((c) => ({ pattern: `/${c.segs.join("/")}`, depths: c.depths }));
72
148
  }
73
149
  /**
74
- * The **canonical** single pattern for a route file — all optional segments present. A file with no
150
+ * The **canonical** single pattern for a route file - all optional segments present. A file with no
75
151
  * optionals yields its one pattern. Use {@link filePathToPatterns} to get every pattern (optionals
76
152
  * expand the set).
77
153
  */
@@ -80,6 +156,26 @@ export function filePathToPattern(file) {
80
156
  return patterns[patterns.length - 1];
81
157
  }
82
158
  /** The dir chain from root → the file's own dir, e.g. "a/b/p.tsx" → ["", "a", "a/b"]. */
159
+ /**
160
+ * Param names in the first `count` segments of a pattern - the params a layout at that depth owns.
161
+ *
162
+ * This is the whole scoping mechanism: a layout wraps a URL prefix, so the params inside that prefix
163
+ * are the ones it may read, and the ones whose change should re-run its loader. Everything deeper
164
+ * belongs to routes below it and must not invalidate it.
165
+ */
166
+ function paramsInPrefix(pattern, count) {
167
+ if (count === 0)
168
+ return [];
169
+ const names = [];
170
+ const segments = pattern === "/" ? [] : pattern.slice(1).split("/");
171
+ for (const segment of segments.slice(0, count)) {
172
+ // Mirrors the router's own grammar: `:name`, `*name`, and a mixed segment's embedded `:name`s.
173
+ for (const match of segment.matchAll(/[:*]([A-Za-z_][A-Za-z0-9_]*)/g)) {
174
+ names.push(match[1]);
175
+ }
176
+ }
177
+ return names;
178
+ }
83
179
  const ancestorDirs = (file) => {
84
180
  const dirs = [""];
85
181
  const dir = dirOf(file);
@@ -94,7 +190,7 @@ const ancestorDirs = (file) => {
94
190
  };
95
191
  /**
96
192
  * Build a manifest from route file paths (relative to the routes dir) + an `importer` that
97
- * turns a path into a lazy module loader. Pure — no fs. Throws at boot (the loud-and-early
193
+ * turns a path into a lazy module loader. Pure - no fs. Throws at boot (the loud-and-early
98
194
  * RouteConfigError ethos) on duplicate patterns. `_layout`/`_404`/`_error` files are special; other
99
195
  * `_`-prefixed files are ignored (private/colocated, never routed).
100
196
  */
@@ -104,6 +200,7 @@ export function buildManifest(files, importer) {
104
200
  const errorDirs = new Set();
105
201
  const errors = {};
106
202
  let notFound;
203
+ const statusPages = {};
107
204
  const routeFiles = [];
108
205
  for (const file of files) {
109
206
  const stem = stripExt(baseName(file));
@@ -120,6 +217,12 @@ export function buildManifest(files, importer) {
120
217
  else if (stem === "_404") {
121
218
  notFound = { file, load: importer(file) };
122
219
  }
220
+ else if (STATUS_PAGE.test(stem) && dirOf(file) === "") {
221
+ // `_410.tsx`, `_451.tsx`, … at the routes root. Root-only: unlike `_error`, these are not
222
+ // resolved per segment - a terminal status is a property of the outcome, not of where in the
223
+ // tree the route lives, and a per-segment variant would need a precedence rule nobody asked for.
224
+ statusPages[stem.slice(1)] = { file, load: importer(file) };
225
+ }
123
226
  else if (!stem.startsWith("_")) {
124
227
  routeFiles.push(file);
125
228
  }
@@ -128,22 +231,29 @@ export function buildManifest(files, importer) {
128
231
  const routes = [];
129
232
  for (const file of routeFiles) {
130
233
  const dirs = ancestorDirs(file);
131
- const layoutIds = dirs.filter((dir) => layoutDirs.has(dir)).map(layoutIdFor);
234
+ const layoutDirsForFile = dirs.filter((dir) => layoutDirs.has(dir));
235
+ const layoutIds = layoutDirsForFile.map(layoutIdFor);
132
236
  const errorIds = dirs.filter((dir) => errorDirs.has(dir)).map(errorIdFor);
133
237
  const id = stripExt(file);
134
238
  const load = importer(file); // one lazy loader per file, shared by its (possibly expanded) patterns
135
239
  // An optional `[[x]]` segment expands a file into multiple patterns, all pointing at the same
136
240
  // module (same id/load/layout chain). Distinct patterns ⇒ no match ambiguity (different lengths).
137
- for (const pattern of filePathToPatterns(file)) {
241
+ for (const { pattern, depths } of filePathToRoutes(file)) {
138
242
  const existing = byPattern.get(pattern);
139
243
  if (existing !== undefined) {
140
244
  throw new Error(`[nifra/web] duplicate route: "${file}" and "${existing}" both map to "${pattern}"`);
141
245
  }
142
246
  byPattern.set(pattern, file);
143
- routes.push({ id, pattern, layoutIds, errorIds, file, load });
247
+ const layoutParams = layoutDirsForFile.map((dir) => paramsInPrefix(pattern, depths[dir === "" ? 0 : dir.split("/").length] ?? 0));
248
+ routes.push({ id, pattern, layoutIds, layoutParams, errorIds, file, load });
144
249
  }
145
250
  }
146
- const base = { routes, layouts, errors };
251
+ const base = {
252
+ routes,
253
+ layouts,
254
+ errors,
255
+ ...(Object.keys(statusPages).length > 0 ? { statusPages } : {}),
256
+ };
147
257
  return notFound === undefined ? base : { ...base, notFound };
148
258
  }
149
259
  const FILL_PARAM = /:([A-Za-z_][A-Za-z0-9_]*)/g;
@@ -173,7 +283,7 @@ export function fillRoutePattern(pattern, params) {
173
283
  return { path, missing };
174
284
  }
175
285
  /**
176
- * Enumerate the static-routing facts `prerenderRoutes` would produce — static routes opted in via
286
+ * Enumerate the static-routing facts `prerenderRoutes` would produce - static routes opted in via
177
287
  * `export const prerender = true`, each `getStaticPaths` entry of a dynamic route, and each dynamic
178
288
  * route's `fallback` policy. Pure (no rendering), so a server can compute what to hand `createWebApp`
179
289
  * (the prerendered set for the client's static-`_data.json` soft-nav + the fallback map for