@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.
- package/README.md +3 -3
- package/dist/build-vite.d.ts +68 -0
- package/dist/build-vite.d.ts.map +1 -0
- package/dist/build-vite.js +433 -0
- package/dist/build-vite.js.map +1 -0
- package/dist/build.d.ts +174 -65
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +376 -135
- package/dist/build.js.map +1 -1
- package/dist/bun-dev-entry.d.ts +83 -0
- package/dist/bun-dev-entry.d.ts.map +1 -0
- package/dist/bun-dev-entry.js +136 -0
- package/dist/bun-dev-entry.js.map +1 -0
- package/dist/chunk-names.js +1 -1
- package/dist/client.d.ts +3 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +209 -27
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +2 -2
- package/dist/conformance.js +3 -3
- package/dist/deferred.d.ts +11 -11
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +16 -16
- package/dist/dev-error.d.ts +2 -2
- package/dist/dev-error.js +3 -3
- package/dist/dev-port.d.ts +36 -0
- package/dist/dev-port.d.ts.map +1 -0
- package/dist/dev-port.js +48 -0
- package/dist/dev-port.js.map +1 -0
- package/dist/dev.d.ts +62 -5
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +419 -60
- package/dist/dev.js.map +1 -1
- package/dist/draft.d.ts +44 -7
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +92 -9
- package/dist/draft.js.map +1 -1
- package/dist/fn-state.d.ts +56 -0
- package/dist/fn-state.d.ts.map +1 -0
- package/dist/fn-state.js +77 -0
- package/dist/fn-state.js.map +1 -0
- package/dist/fn.d.ts +111 -0
- package/dist/fn.d.ts.map +1 -0
- package/dist/fn.js +125 -0
- package/dist/fn.js.map +1 -0
- package/dist/fonts-google.d.ts +9 -9
- package/dist/fonts-google.js +10 -10
- package/dist/fonts.d.ts +7 -7
- package/dist/fonts.js +1 -1
- package/dist/forms.d.ts +8 -8
- package/dist/fs.d.ts +15 -2
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +8 -2
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +266 -74
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +869 -179
- package/dist/index.js.map +1 -1
- package/dist/internal/head-attributes.d.ts +15 -0
- package/dist/internal/head-attributes.d.ts.map +1 -0
- package/dist/internal/head-attributes.js +99 -0
- package/dist/internal/head-attributes.js.map +1 -0
- package/dist/internal/script-types.d.ts +14 -0
- package/dist/internal/script-types.d.ts.map +1 -0
- package/dist/internal/script-types.js +17 -0
- package/dist/internal/script-types.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +60 -0
- package/dist/internal/server-boundary.d.ts.map +1 -0
- package/dist/internal/server-boundary.js +174 -0
- package/dist/internal/server-boundary.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +16 -0
- package/dist/internal/timing-safe-equal.d.ts.map +1 -0
- package/dist/internal/timing-safe-equal.js +26 -0
- package/dist/internal/timing-safe-equal.js.map +1 -0
- package/dist/internal/vite-import.d.ts +63 -0
- package/dist/internal/vite-import.d.ts.map +1 -0
- package/dist/internal/vite-import.js +94 -0
- package/dist/internal/vite-import.js.map +1 -0
- package/dist/islands.d.ts +5 -5
- package/dist/islands.js +5 -5
- package/dist/isr.d.ts +15 -15
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +8 -21
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +12 -6
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +121 -40
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +136 -26
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +91 -0
- package/dist/module-graph.d.ts.map +1 -0
- package/dist/module-graph.js +92 -0
- package/dist/module-graph.js.map +1 -0
- package/dist/navigation.d.ts +120 -5
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +37 -5
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/css-modules.d.ts +3 -3
- package/dist/plugins/css-modules.d.ts.map +1 -1
- package/dist/plugins/css-modules.js +0 -0
- package/dist/plugins/css-modules.js.map +1 -1
- package/dist/plugins/kit.d.ts +8 -8
- package/dist/plugins/kit.js +9 -9
- package/dist/plugins/scss.js +3 -3
- package/dist/plugins/vite-leak-guard.d.ts +45 -0
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -0
- package/dist/plugins/vite-leak-guard.js +67 -0
- package/dist/plugins/vite-leak-guard.js.map +1 -0
- package/dist/plugins/vite-server-fn.d.ts +23 -0
- package/dist/plugins/vite-server-fn.d.ts.map +1 -0
- package/dist/plugins/vite-server-fn.js +51 -0
- package/dist/plugins/vite-server-fn.js.map +1 -0
- package/dist/plugins/vite-server-only.d.ts +49 -0
- package/dist/plugins/vite-server-only.d.ts.map +1 -0
- package/dist/plugins/vite-server-only.js +57 -0
- package/dist/plugins/vite-server-only.js.map +1 -0
- package/dist/prerender.d.ts +5 -5
- package/dist/prerender.js +7 -7
- package/dist/public-dir.d.ts +36 -0
- package/dist/public-dir.d.ts.map +1 -0
- package/dist/public-dir.js +89 -0
- package/dist/public-dir.js.map +1 -0
- package/dist/query.d.ts +10 -10
- package/dist/query.js +15 -15
- package/dist/route-manifest.d.ts +91 -0
- package/dist/route-manifest.d.ts.map +1 -0
- package/dist/route-manifest.js +128 -0
- package/dist/route-manifest.js.map +1 -0
- package/dist/router.d.ts +94 -22
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +170 -58
- package/dist/router.js.map +1 -1
- package/dist/search.d.ts +94 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +232 -0
- package/dist/search.js.map +1 -0
- package/dist/server-only.d.ts +6 -6
- package/dist/server-only.js +7 -7
- package/dist/service-worker.d.ts +56 -0
- package/dist/service-worker.d.ts.map +1 -0
- package/dist/service-worker.js +127 -0
- package/dist/service-worker.js.map +1 -0
- package/dist/vite.d.ts +25 -17
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +75 -33
- package/dist/vite.js.map +1 -1
- package/package.json +48 -4
- package/server-manifest.d.ts +31 -0
- package/src/build-vite.ts +553 -0
- package/src/build.ts +563 -172
- package/src/bun-dev-entry.ts +184 -0
- package/src/chunk-names.ts +1 -1
- package/src/client.ts +221 -30
- package/src/conformance.ts +3 -3
- package/src/deferred.ts +17 -17
- package/src/dev-error.ts +3 -3
- package/src/dev-port.ts +63 -0
- package/src/dev.ts +468 -66
- package/src/draft.ts +124 -12
- package/src/fn-state.ts +107 -0
- package/src/fn.ts +199 -0
- package/src/fonts-google.ts +13 -13
- package/src/fonts.ts +7 -7
- package/src/forms.ts +8 -8
- package/src/fs.ts +24 -6
- package/src/index.ts +1174 -225
- package/src/internal/head-attributes.ts +109 -0
- package/src/internal/script-types.ts +18 -0
- package/src/internal/server-boundary.ts +187 -0
- package/src/internal/timing-safe-equal.ts +24 -0
- package/src/internal/vite-import.ts +92 -0
- package/src/islands.ts +7 -7
- package/src/isr.ts +18 -30
- package/src/llms-txt.ts +13 -5
- package/src/manifest.ts +264 -48
- package/src/module-graph.ts +146 -0
- package/src/navigation.ts +151 -5
- package/src/plugins/css-modules.ts +0 -0
- package/src/plugins/kit.ts +11 -11
- package/src/plugins/scss.ts +3 -3
- package/src/plugins/vite-leak-guard.ts +110 -0
- package/src/plugins/vite-server-fn.ts +67 -0
- package/src/plugins/vite-server-only.ts +75 -0
- package/src/prerender.ts +10 -10
- package/src/public-dir.ts +108 -0
- package/src/query.ts +22 -22
- package/src/route-manifest.ts +240 -0
- package/src/router.ts +288 -74
- package/src/search.ts +297 -0
- package/src/server-only.ts +7 -7
- package/src/service-worker.ts +165 -0
- package/src/vite.ts +103 -49
package/src/llms-txt.ts
CHANGED
|
@@ -91,11 +91,19 @@ export async function generateLlmsTxt(
|
|
|
91
91
|
// 1. Project Guidelines
|
|
92
92
|
let agentsMd = ""
|
|
93
93
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
// Edge bundles define this flag and dead-code-eliminate the filesystem branch. Keeping the import
|
|
95
|
+
// behind a runtime-only `typeof Bun` check is insufficient: Rollup must still emit an unresolved
|
|
96
|
+
// `node:` dependency, which workerd cannot load.
|
|
97
|
+
const edgeRuntime = (
|
|
98
|
+
globalThis as typeof globalThis & { readonly __NIFRA_EDGE_RUNTIME__?: boolean }
|
|
99
|
+
).__NIFRA_EDGE_RUNTIME__
|
|
100
|
+
if (edgeRuntime !== true) {
|
|
101
|
+
if (typeof Bun !== "undefined") {
|
|
102
|
+
agentsMd = await Bun.file("AGENTS.md").text()
|
|
103
|
+
} else {
|
|
104
|
+
const fs = await import("node:fs/promises")
|
|
105
|
+
agentsMd = await fs.readFile("AGENTS.md", "utf-8")
|
|
106
|
+
}
|
|
99
107
|
}
|
|
100
108
|
} catch {
|
|
101
109
|
// ignore
|
package/src/manifest.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Route manifest
|
|
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
|
|
|
8
9
|
/** Context passed to a route `loader`. The `api` + `env` are injected by `createWebApp` and typed
|
|
9
10
|
* per-route via `@nifrajs/client`'s `LoaderArgs<Api, Env>` (here they are opaque to the agnostic core). */
|
|
10
11
|
export interface LoaderContext {
|
|
11
12
|
readonly params: Record<string, string>
|
|
12
13
|
readonly request: Request
|
|
13
|
-
/** Alias of {@link request}
|
|
14
|
+
/** Alias of {@link request} - mirrors a route handler's `c.req` so the same name works in both. */
|
|
14
15
|
readonly req: Request
|
|
15
16
|
readonly api: unknown
|
|
16
17
|
/** Platform bindings forwarded from the request `c.env` (Workers env/KV/D1). Opaque here. */
|
|
@@ -18,6 +19,10 @@ export interface LoaderContext {
|
|
|
18
19
|
/** `true` when the request carries a valid draft/preview cookie (only when `createWebApp` is given a
|
|
19
20
|
* `draftSecret`; otherwise always `false`). Branch on it to load unpublished content for editors. */
|
|
20
21
|
readonly draft: boolean
|
|
22
|
+
/** The URL search params, validated against the route's `searchSchema` (a Standard Schema) when it
|
|
23
|
+
* declares one - failing closed to the schema's defaults - else the raw parsed query. Typed per-route
|
|
24
|
+
* via `@nifrajs/client`'s `LoaderArgs<Api, Env, Search>`. */
|
|
25
|
+
readonly search: Record<string, unknown>
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/** A route's optional data loader: params/request in, data out. */
|
|
@@ -25,20 +30,20 @@ export type Loader = (ctx: LoaderContext) => unknown | Promise<unknown>
|
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* A route's optional mutation, run on POST. Shares the loader context (params/request/api);
|
|
28
|
-
* read the form/JSON body off `request`. Returns either a `Response` (e.g. a redirect
|
|
33
|
+
* read the form/JSON body off `request`. Returns either a `Response` (e.g. a redirect -
|
|
29
34
|
* passed straight through) or data, surfaced to the page component as `actionData`.
|
|
30
35
|
*/
|
|
31
36
|
export type Action = (ctx: LoaderContext) => unknown | Promise<unknown>
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
39
|
* One `<link>` tag's attributes for a route/layout's `meta.link`. The common HTML `<link>` attributes
|
|
35
|
-
* are spelled out and **optional** so a typed partial like `{ rel, href, hreflang }` is assignable
|
|
40
|
+
* are spelled out and **optional** so a typed partial like `{ rel, href, hreflang }` is assignable -
|
|
36
41
|
* the previous `Record<string, string>` required *every* value to be a present string, which rejected
|
|
37
|
-
* exactly that idiomatic shape (the bug this fixes).
|
|
38
|
-
*
|
|
39
|
-
* `disabled` (rendered
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
+
* exactly that idiomatic shape (the bug this fixes). Standard attributes are explicit and the template
|
|
43
|
+
* index signature admits inert `data-*` metadata without opening executable `on*` attributes. `boolean`
|
|
44
|
+
* covers `disabled` (rendered bare when `true`, omitted when `false`), and `undefined` lets a caller
|
|
45
|
+
* spread in a conditionally absent attribute. SSR and soft navigation apply one runtime allowlist too,
|
|
46
|
+
* so a cast or untyped route cannot widen the injection surface.
|
|
42
47
|
*/
|
|
43
48
|
export interface LinkDescriptor {
|
|
44
49
|
readonly rel?: string
|
|
@@ -46,6 +51,7 @@ export interface LinkDescriptor {
|
|
|
46
51
|
readonly hreflang?: string
|
|
47
52
|
readonly crossorigin?: string
|
|
48
53
|
readonly media?: string
|
|
54
|
+
readonly nonce?: string
|
|
49
55
|
readonly sizes?: string
|
|
50
56
|
readonly type?: string
|
|
51
57
|
readonly as?: string
|
|
@@ -57,43 +63,85 @@ export interface LinkDescriptor {
|
|
|
57
63
|
readonly imagesizes?: string
|
|
58
64
|
readonly color?: string
|
|
59
65
|
readonly disabled?: boolean
|
|
60
|
-
readonly [attr: string]: string |
|
|
66
|
+
readonly [attr: `data-${string}`]: string | undefined
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
/** One `<
|
|
69
|
+
/** One managed `<meta>` tag. Standard attributes and inert `data-*` metadata only. */
|
|
70
|
+
export interface MetaDescriptor {
|
|
71
|
+
readonly charset?: string
|
|
72
|
+
readonly content?: string
|
|
73
|
+
readonly "http-equiv"?: string
|
|
74
|
+
readonly itemprop?: string
|
|
75
|
+
readonly media?: string
|
|
76
|
+
readonly name?: string
|
|
77
|
+
readonly property?: string
|
|
78
|
+
readonly scheme?: string
|
|
79
|
+
readonly [attr: `data-${string}`]: string | undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One `<script>` element a route contributes to `<head>` - for structured data (JSON-LD) and other
|
|
64
83
|
* inert, non-executable head scripts. The `content` is the script body; `type` defaults to
|
|
65
84
|
* `"application/ld+json"` (the common case). The renderer escapes `content` against an HTML breakout
|
|
66
|
-
* (`</`, `<!--`, `]]>`)
|
|
85
|
+
* (`</`, `<!--`, `]]>`) - see `escapeScriptContent` - so a JSON-LD payload can never close the
|
|
67
86
|
* `<script>` element early. `content` is **JSON/text, never raw HTML**: this slot is not an XSS escape
|
|
68
87
|
* hatch for arbitrary markup. */
|
|
88
|
+
export type InertScriptType = "application/ld+json" | "application/json"
|
|
89
|
+
|
|
69
90
|
export interface ScriptDescriptor {
|
|
70
91
|
/** The script's `type` attribute. Default `"application/ld+json"`. */
|
|
71
|
-
readonly type?:
|
|
92
|
+
readonly type?: InertScriptType
|
|
72
93
|
/** The script body (e.g. a `JSON.stringify`'d JSON-LD object). Escaped for safe `<script>` embedding. */
|
|
73
94
|
readonly content: string
|
|
74
95
|
}
|
|
75
96
|
|
|
97
|
+
/** Explicit escape hatch for executable inline code. A CSP nonce is mandatory. */
|
|
98
|
+
export interface UnsafeScriptDescriptor {
|
|
99
|
+
readonly unsafe: true
|
|
100
|
+
readonly type: "module" | "text/javascript"
|
|
101
|
+
readonly nonce: string
|
|
102
|
+
readonly content: string
|
|
103
|
+
}
|
|
104
|
+
|
|
76
105
|
/**
|
|
77
|
-
* The document head a route contributes
|
|
106
|
+
* The document head a route contributes - title + `<meta>`/`<link>`/`<script>` tag sets. Returned by a
|
|
78
107
|
* route/layout `meta` (statically, or from a {@link MetaArgs} function). Every value is serialized into
|
|
79
|
-
* managed (`data-nifra`) head tags: attribute
|
|
80
|
-
*
|
|
81
|
-
* loader-derived strings (LLM-authored `og:*`, user content) are XSS-safe
|
|
108
|
+
* managed (`data-nifra`) head tags: tag-specific attribute allowlists reject event handlers and active
|
|
109
|
+
* URL/refresh contexts, values are HTML-escaped at render, and `script[].content` is breakout-escaped -
|
|
110
|
+
* so loader-derived strings (LLM-authored `og:*`, user content) are XSS-safe by construction.
|
|
82
111
|
* Layout-chain heads merge with the page's via `mergeHeads` (arrays concat outermost→page; `title` is
|
|
83
112
|
* nearest-wins). Build `og:*`/`twitter:*` with `openGraph(...)`, canonical with `canonical(...)`, and
|
|
84
113
|
* JSON-LD with `jsonLd(...)` (all from `@nifrajs/web`) rather than hand-writing the records.
|
|
85
114
|
*/
|
|
86
115
|
export interface Meta {
|
|
87
116
|
readonly title?: string
|
|
88
|
-
readonly meta?:
|
|
117
|
+
readonly meta?: readonly MetaDescriptor[]
|
|
89
118
|
readonly link?: readonly LinkDescriptor[]
|
|
90
119
|
/** Inert head `<script>`s (JSON-LD structured data, etc.). See {@link ScriptDescriptor}. */
|
|
91
120
|
readonly script?: readonly ScriptDescriptor[]
|
|
121
|
+
/** Executable inline scripts. Prefer external modules; construct only with `unsafeInlineScript()`. */
|
|
122
|
+
readonly unsafeScript?: readonly UnsafeScriptDescriptor[]
|
|
123
|
+
/**
|
|
124
|
+
* The document language - `<html lang="...">`. Nearest-wins like `title`, so a localized route can
|
|
125
|
+
* override a layout's default. Defaults to `"en"` when no head in the chain sets it.
|
|
126
|
+
*
|
|
127
|
+
* This is the ONLY way to set it: the shell's `<html>` is framework-owned, so a multilingual app
|
|
128
|
+
* otherwise serves every URL as `lang="en"` - which tells a screen reader to pronounce every locale
|
|
129
|
+
* with an English voice, and suppresses the browser's translation offer.
|
|
130
|
+
*/
|
|
131
|
+
readonly lang?: string
|
|
132
|
+
/**
|
|
133
|
+
* The document writing direction - `<html dir="...">`. Nearest-wins like `title`. Omitted from the
|
|
134
|
+
* shell when unset, which is HTML's `ltr` default, so an LTR app's output is byte-identical.
|
|
135
|
+
*
|
|
136
|
+
* Required for Arabic/Hebrew/Urdu/Persian: without it the browser lays the page out left-to-right
|
|
137
|
+
* and mis-orders trailing punctuation, so the text renders wrong rather than merely unstyled.
|
|
138
|
+
*/
|
|
139
|
+
readonly dir?: "ltr" | "rtl" | "auto"
|
|
92
140
|
}
|
|
93
141
|
|
|
94
142
|
/**
|
|
95
143
|
* Args for a route's `meta` function: the loader's `data` + the route `params` + the request `origin`.
|
|
96
|
-
* `meta()` runs in BOTH SSR and client navigation, so it has **no `request`/`process.env`/server access**
|
|
144
|
+
* `meta()` runs in BOTH SSR and client navigation, so it has **no `request`/`process.env`/server access** -
|
|
97
145
|
* `origin` is the only server-resolved fact it gets (so you needn't thread `siteUrl` through loader data
|
|
98
146
|
* for absolute `og:url`/`canonical`/`og:image` URLs). See {@link origin}.
|
|
99
147
|
*/
|
|
@@ -101,10 +149,10 @@ export interface MetaArgs<Data = unknown> {
|
|
|
101
149
|
readonly data: Data
|
|
102
150
|
readonly params: Record<string, string>
|
|
103
151
|
/**
|
|
104
|
-
* The site origin
|
|
152
|
+
* The site origin - scheme + host (+ port), e.g. `"https://news.example.com"`, **with no trailing
|
|
105
153
|
* slash**. The single piece of server/env knowledge `meta()` otherwise can't see: it runs in BOTH
|
|
106
154
|
* SSR and client navigation, so it has no `request`/`process.env`. The framework resolves it from the
|
|
107
|
-
* request URL during SSR and from `location.origin` on client nav
|
|
155
|
+
* request URL during SSR and from `location.origin` on client nav - and they match, so an absolute
|
|
108
156
|
* `og:url`/`canonical`/`og:image` built from it never drifts between the server-rendered `<head>` and
|
|
109
157
|
* a soft-nav. Use it for absolute URLs (`origin + "/posts/" + slug`) instead of threading `siteUrl`
|
|
110
158
|
* through loader data. Empty string (`""`) when the origin is unknown (e.g. a hand-built test render
|
|
@@ -114,7 +162,7 @@ export interface MetaArgs<Data = unknown> {
|
|
|
114
162
|
}
|
|
115
163
|
|
|
116
164
|
/** A route's `meta`: a static {@link Meta}, or a function of the loader data + params + the request
|
|
117
|
-
* origin ({@link MetaArgs}). Use the `origin` arg for absolute `canonical`/`og:url`/`og:image` URLs
|
|
165
|
+
* origin ({@link MetaArgs}). Use the `origin` arg for absolute `canonical`/`og:url`/`og:image` URLs -
|
|
118
166
|
* it's resolved server-side from the request and matches the client's `location.origin`. */
|
|
119
167
|
export type MetaInput = Meta | ((args: MetaArgs) => Meta)
|
|
120
168
|
|
|
@@ -129,7 +177,7 @@ export interface StaticPaths {
|
|
|
129
177
|
readonly paths: readonly StaticPath[]
|
|
130
178
|
/**
|
|
131
179
|
* How a path NOT in `paths` is handled. `"ssr"` (default) → rendered on-demand by the worker
|
|
132
|
-
* (the natural hybrid behavior
|
|
180
|
+
* (the natural hybrid behavior - an unlisted path simply isn't a static file); `"404"` → only the
|
|
133
181
|
* listed paths exist. Recorded by `prerenderRoutes` for the deploy layer.
|
|
134
182
|
*/
|
|
135
183
|
readonly fallback?: "ssr" | "404"
|
|
@@ -138,11 +186,21 @@ export interface StaticPaths {
|
|
|
138
186
|
/** A dynamic route's build-time param enumeration (the SSG equivalent of "which pages exist"). */
|
|
139
187
|
export type GetStaticPaths = () => StaticPaths | Promise<StaticPaths>
|
|
140
188
|
|
|
141
|
-
/** A route module
|
|
189
|
+
/** A route module - the default component + optional loader / action / meta. */
|
|
142
190
|
export interface RouteModule {
|
|
143
191
|
readonly default: unknown
|
|
144
192
|
readonly loader?: Loader
|
|
145
193
|
readonly action?: Action
|
|
194
|
+
/** A Standard Schema validating this route's URL search params. When present, `ctx.search` is parsed +
|
|
195
|
+
* validated against it (failing closed to the schema's defaults on invalid input); type it into the
|
|
196
|
+
* loader with `LoaderArgs<Api, Env, typeof searchSchema>`. */
|
|
197
|
+
readonly searchSchema?: StandardSchemaV1<unknown, Record<string, unknown>>
|
|
198
|
+
/** Search keys that are purely client-side UI (`"tab"`, a client-side `"sort"`, `"modal"`) and do NOT
|
|
199
|
+
* affect this route's loader. When a client navigation stays on the same route + pathname and changes
|
|
200
|
+
* ONLY these keys, the router updates the URL (so `useSearch` re-renders) WITHOUT re-running the loader.
|
|
201
|
+
* Any other key change revalidates as usual - so omitting a data-affecting key here can never serve
|
|
202
|
+
* stale data, it only forgoes the optimization. Client-only: the server always renders fresh. */
|
|
203
|
+
readonly searchClientKeys?: readonly string[]
|
|
146
204
|
readonly meta?: MetaInput
|
|
147
205
|
/**
|
|
148
206
|
* Opt this route out of nifra's full-document client hydration. The server still renders the full
|
|
@@ -154,12 +212,12 @@ export interface RouteModule {
|
|
|
154
212
|
/**
|
|
155
213
|
* Opt a **static** route (no `:param`/`*`) into build-time prerendering (SSG): `prerenderRoutes`
|
|
156
214
|
* (from `@nifrajs/web/build`) renders it to a static `index.html` at build. The loader runs at build
|
|
157
|
-
* with the in-process `api` (build-safe data only
|
|
215
|
+
* with the in-process `api` (build-safe data only - no per-request cookies/secrets); `defer()` on a
|
|
158
216
|
* prerendered route resolves at build. For **dynamic** routes use {@link getStaticPaths} instead.
|
|
159
217
|
*/
|
|
160
218
|
readonly prerender?: boolean
|
|
161
219
|
/**
|
|
162
|
-
* Enumerate the concrete params to prerender for a **dynamic** (`:param`) route
|
|
220
|
+
* Enumerate the concrete params to prerender for a **dynamic** (`:param`) route - the SSG path list
|
|
163
221
|
* (blogs/docs/etc.). Runs at build; `prerenderRoutes` renders one `index.html` per returned path.
|
|
164
222
|
*/
|
|
165
223
|
readonly getStaticPaths?: GetStaticPaths
|
|
@@ -171,7 +229,7 @@ export interface RouteModule {
|
|
|
171
229
|
readonly revalidate?: number
|
|
172
230
|
/**
|
|
173
231
|
* No-framework island bundles (`@nifrajs/web/islands`) to load on this route, as `<script
|
|
174
|
-
* type="module">` in the document tail. Loaded **regardless of `hydrate`**
|
|
232
|
+
* type="module">` in the document tail. Loaded **regardless of `hydrate`** - pair with
|
|
175
233
|
* `export const hydrate = false` for a static page that ships zero framework JS and mounts
|
|
176
234
|
* interactivity through `<Island>` markers + `mountIslands` enhancers instead.
|
|
177
235
|
*/
|
|
@@ -181,7 +239,7 @@ export interface RouteModule {
|
|
|
181
239
|
/** A layout (or `_404`/`_error`) entry: its source file (for client codegen) + a lazy loader.
|
|
182
240
|
*
|
|
183
241
|
* A `_layout.tsx` may export `meta` (static {@link Meta} or a function of the loader data + params,
|
|
184
|
-
* same shape as a route's) to contribute sitewide `<head>` tags
|
|
242
|
+
* same shape as a route's) to contribute sitewide `<head>` tags - `hreflang`/`preconnect`/etc. that
|
|
185
243
|
* belong on every page under the layout. The layout chain's heads merge with the page's: arrays
|
|
186
244
|
* (`meta`/`link`) concatenate outermost→innermost→page; scalars (`title`) are nearest-wins (the page
|
|
187
245
|
* overrides an inner layout, which overrides an outer one). See `mergeHeads` in `@nifrajs/web`. */
|
|
@@ -195,6 +253,16 @@ export interface RouteEntry {
|
|
|
195
253
|
readonly id: string
|
|
196
254
|
readonly pattern: string
|
|
197
255
|
readonly layoutIds: readonly string[]
|
|
256
|
+
/**
|
|
257
|
+
* Param names each layout in {@link layoutIds} owns, aligned by index.
|
|
258
|
+
*
|
|
259
|
+
* A layout wraps a URL prefix, so these are the params it may read and the ones whose change should
|
|
260
|
+
* re-run its loader - everything deeper belongs to routes beneath it. Derived per (route, layout)
|
|
261
|
+
* pair rather than per layout, because one layout can own different params on different expanded
|
|
262
|
+
* patterns: `[[lang]]/docs/_layout` owns nothing on `/docs/:slug` and `{lang}` on `/:lang/docs/:slug`.
|
|
263
|
+
* Optional only so hand-built test manifests may omit it.
|
|
264
|
+
*/
|
|
265
|
+
readonly layoutParams?: ReadonlyArray<readonly string[]>
|
|
198
266
|
/** `_error` boundary ids in this route's ancestor chain (outermost → innermost). The last is the
|
|
199
267
|
* **nearest** boundary, rendered when the route's loader throws. Always set by `buildManifest`
|
|
200
268
|
* (optional only so hand-built test manifests may omit it); absent/empty ⇒ no boundary (error 500s). */
|
|
@@ -211,11 +279,16 @@ export interface Manifest {
|
|
|
211
279
|
* `buildManifest` (optional only so hand-built test manifests may omit it). */
|
|
212
280
|
readonly errors?: Readonly<Record<string, LayoutEntry>>
|
|
213
281
|
readonly notFound?: LayoutEntry
|
|
282
|
+
/** Per-status terminal pages from `_<status>.tsx` at the routes root (`_410`, `_451`, …), keyed by
|
|
283
|
+
* the status as a string. Rendered by a loader's `gone()` / `statusPage(n)`; a status with no page
|
|
284
|
+
* falls back to `_404`, and then to plain text. `_404` itself stays on {@link notFound} - it is
|
|
285
|
+
* reached by unmatched paths too, which these are not. */
|
|
286
|
+
readonly statusPages?: Readonly<Record<string, LayoutEntry>>
|
|
214
287
|
}
|
|
215
288
|
|
|
216
289
|
// `.svelte` and `.vue` routes are supported too: their `default` export is the component and
|
|
217
290
|
// `loader`/`action`/`meta` come from a module-level script block (Svelte `<script module>`, Vue's plain
|
|
218
|
-
// `<script>`) as named ESM exports
|
|
291
|
+
// `<script>`) as named ESM exports - the same RouteModule shape as `.tsx`. Both compile via their
|
|
219
292
|
// package's Bun plugin (`@nifrajs/web-svelte/plugin`, `@nifrajs/web-vue/plugin`).
|
|
220
293
|
const ROUTE_EXT = /\.(tsx|jsx|svelte|vue|mdx)$/
|
|
221
294
|
const PARAM = /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/
|
|
@@ -224,8 +297,12 @@ const CATCH_ALL = /^\[\.\.\.([A-Za-z_][A-Za-z0-9_]*)\]$/
|
|
|
224
297
|
// two patterns (`:lang` present / absent), so `[[lang]]/about` serves both `/about` and `/en/about`.
|
|
225
298
|
const OPTIONAL = /^\[\[([A-Za-z_][A-Za-z0-9_]*)\]\]$/
|
|
226
299
|
// A route group: a `(name)` folder organizes routes (and can hold its own `_layout`) without
|
|
227
|
-
// contributing a URL segment
|
|
300
|
+
// contributing a URL segment - mirrors Next/Remix. Requires content between the parens.
|
|
228
301
|
const GROUP = /^\(.+\)$/
|
|
302
|
+
// A terminal status page: `_410.tsx`, `_451.tsx`. `_404` is matched before this and stays its own
|
|
303
|
+
// thing. Restricted to 3 digits so `_401k` or `_4` is treated as an ordinary underscore-prefixed
|
|
304
|
+
// file (ignored) rather than silently becoming a status page.
|
|
305
|
+
const STATUS_PAGE = /^_[1-5][0-9][0-9]$/
|
|
229
306
|
|
|
230
307
|
const stripExt = (file: string): string => file.replace(ROUTE_EXT, "")
|
|
231
308
|
const baseName = (file: string): string => file.slice(file.lastIndexOf("/") + 1)
|
|
@@ -238,18 +315,116 @@ const errorIdFor = (dir: string): string => (dir === "" ? "_error" : `${dir}/_er
|
|
|
238
315
|
* Derive **every** nifra router pattern a route file maps to (relative to the routes dir):
|
|
239
316
|
* `index` → the parent path, `[id]` → `:id`, `[...slug]` → `*slug` (catch-all, captures the rest of
|
|
240
317
|
* the path into one param), `(group)` folders are dropped from the URL (organization only), and an
|
|
241
|
-
* optional `[[lang]]` expands the set
|
|
318
|
+
* optional `[[lang]]` expands the set - once with the segment present (`:lang`) and once absent. A file
|
|
242
319
|
* with no optionals yields exactly one pattern. Throws on an invalid param or a catch-all that isn't
|
|
243
320
|
* the last segment.
|
|
244
321
|
*/
|
|
322
|
+
/** Every `[…]` marker in a file segment, so a part-literal segment can be spliced into a pattern. */
|
|
323
|
+
const FILE_MARKER = /\[([^\]]*)\]/g
|
|
324
|
+
const FILE_PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Translate a part-literal-part-`[param]` file segment into its route pattern, or `undefined` when
|
|
328
|
+
* the segment is wholly literal or wholly a marker (both already handled by the caller).
|
|
329
|
+
*
|
|
330
|
+
* `[inKey].txt` becomes `:inKey.txt`. Needed because IndexNow-style verification files, per-locale
|
|
331
|
+
* sitemaps, and static-site URL parity (`post-[id].html`) all put a variable INSIDE a filename, and
|
|
332
|
+
* `stripExt` already reduces `[inKey].txt.tsx` to `[inKey].txt` - `robots.txt.tsx` proves literal
|
|
333
|
+
* dots survive into a segment, so this was only ever a parser gap.
|
|
334
|
+
*
|
|
335
|
+
* `[[optional]]` and `[...catchAll]` are **rejected** inside a mixed segment rather than given a
|
|
336
|
+
* meaning. There is no sensible absent form for `/[[locale]]-feed.xml` - `/-feed.xml` and dropping
|
|
337
|
+
* the segment are both surprising - and a catch-all captures the remaining path verbatim, which
|
|
338
|
+
* conflicts with a trailing literal. A clear rejection beats semantics nobody can predict.
|
|
339
|
+
*/
|
|
340
|
+
function mixedFileSegment(seg: string, file: string): string | undefined {
|
|
341
|
+
// A segment that is ENTIRELY one of the existing whole-segment forms belongs to the caller, not
|
|
342
|
+
// here. Checked against the real patterns rather than by comparing the first marker to the
|
|
343
|
+
// segment: `[[lang]]` does not equal its own first `[…]` match, so a naive comparison sends every
|
|
344
|
+
// optional segment down the mixed path and rejects it.
|
|
345
|
+
if (PARAM.test(seg) || OPTIONAL.test(seg) || CATCH_ALL.test(seg)) return undefined
|
|
346
|
+
const markers = [...seg.matchAll(FILE_MARKER)]
|
|
347
|
+
if (markers.length === 0) return undefined
|
|
348
|
+
|
|
349
|
+
let pattern = ""
|
|
350
|
+
let cursor = 0
|
|
351
|
+
for (const marker of markers) {
|
|
352
|
+
const inner = marker[1] ?? ""
|
|
353
|
+
if (inner.startsWith("...")) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`[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.`,
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
if (inner.startsWith("[")) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`[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.`,
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
if (!FILE_PARAM_NAME.test(inner)) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`[nifra/web] invalid route param in "${file}": "[${inner}]" must be [name], [[name]], [...name], or a (group) folder`,
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
pattern += `${seg.slice(cursor, marker.index)}:${inner}`
|
|
369
|
+
cursor = (marker.index ?? 0) + marker[0].length
|
|
370
|
+
}
|
|
371
|
+
return pattern + seg.slice(cursor)
|
|
372
|
+
}
|
|
373
|
+
|
|
245
374
|
export function filePathToPatterns(file: string): string[] {
|
|
246
|
-
|
|
247
|
-
|
|
375
|
+
return filePathToRoutes(file).map((route) => route.pattern)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** One URL pattern a route file expands to, plus the scope information layouts need. */
|
|
379
|
+
export interface FileRoutePattern {
|
|
380
|
+
readonly pattern: string
|
|
381
|
+
/**
|
|
382
|
+
* URL segments contributed by the first *k* path parts of the file, indexed by *k*.
|
|
383
|
+
*
|
|
384
|
+
* This is what makes a layout's scope derivable without turning layouts into router nodes. A layout
|
|
385
|
+
* lives at a directory; the segments that directory contributes are exactly the prefix of the URL it
|
|
386
|
+
* wraps, so `depths[partsIn(layoutDir)]` is how many leading pattern segments it owns - and the
|
|
387
|
+
* params inside that prefix are the ones it may read.
|
|
388
|
+
*
|
|
389
|
+
* Per EXPANDED pattern, not per file, because the two diverge: `[[lang]]/docs/_layout` owns nothing
|
|
390
|
+
* on `/docs/:slug` and `{lang}` on `/:lang/docs/:slug`. A route group contributes no segment, so
|
|
391
|
+
* `(marketing)/_layout` owns nothing at all.
|
|
392
|
+
*/
|
|
393
|
+
readonly depths: readonly number[]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Expand a route file into its URL patterns, each carrying per-directory segment counts. */
|
|
397
|
+
export function filePathToRoutes(file: string): FileRoutePattern[] {
|
|
398
|
+
// Each combo is the URL segments built so far, plus the running per-path-part segment count.
|
|
399
|
+
let combos: Array<{ segs: string[]; depths: number[] }> = [{ segs: [], depths: [0] }]
|
|
248
400
|
const segments = stripExt(file).split("/")
|
|
401
|
+
// Record the running segment count after each path part, on every combo. Called once per part so
|
|
402
|
+
// `depths` stays index-aligned with the file's path parts even for parts that emit nothing.
|
|
403
|
+
const mark = (): void => {
|
|
404
|
+
for (const combo of combos) combo.depths.push(combo.segs.length)
|
|
405
|
+
}
|
|
406
|
+
const push = (segment: string): void => {
|
|
407
|
+
for (const combo of combos) combo.segs.push(segment)
|
|
408
|
+
}
|
|
249
409
|
for (let i = 0; i < segments.length; i++) {
|
|
250
410
|
const seg = segments[i]!
|
|
251
|
-
if (seg === "index")
|
|
252
|
-
|
|
411
|
+
if (seg === "index") {
|
|
412
|
+
mark()
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
if (GROUP.test(seg)) {
|
|
416
|
+
// Route group - no URL segment (its `_layout` still applies, keyed by dir).
|
|
417
|
+
mark()
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
// A segment that is part literal, part `[param]` - `[inKey].txt`, `post-[id].html`. Checked
|
|
421
|
+
// before the whole-segment forms below, which only handle a marker spanning the entire segment.
|
|
422
|
+
const mixed = mixedFileSegment(seg, file)
|
|
423
|
+
if (mixed !== undefined) {
|
|
424
|
+
push(mixed)
|
|
425
|
+
mark()
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
253
428
|
if (seg.startsWith("[")) {
|
|
254
429
|
const catchAll = CATCH_ALL.exec(seg)
|
|
255
430
|
if (catchAll !== null) {
|
|
@@ -261,13 +436,18 @@ export function filePathToPatterns(file: string): string[] {
|
|
|
261
436
|
`[nifra/web] catch-all "${seg}" must be the last segment in "${file}" (found "${after.join("/")}" after it)`,
|
|
262
437
|
)
|
|
263
438
|
}
|
|
264
|
-
|
|
439
|
+
push(`*${catchAll[1]}`)
|
|
440
|
+
mark()
|
|
265
441
|
continue
|
|
266
442
|
}
|
|
267
443
|
const optional = OPTIONAL.exec(seg)
|
|
268
444
|
if (optional !== null) {
|
|
269
445
|
// Optional segment: keep each existing combo (absent) AND a copy with `:name` appended (present).
|
|
270
|
-
combos = combos.flatMap((c) => [
|
|
446
|
+
combos = combos.flatMap((c) => [
|
|
447
|
+
{ segs: c.segs, depths: c.depths },
|
|
448
|
+
{ segs: [...c.segs, `:${optional[1]}`], depths: [...c.depths] },
|
|
449
|
+
])
|
|
450
|
+
mark()
|
|
271
451
|
continue
|
|
272
452
|
}
|
|
273
453
|
const match = PARAM.exec(seg)
|
|
@@ -276,16 +456,17 @@ export function filePathToPatterns(file: string): string[] {
|
|
|
276
456
|
`[nifra/web] invalid route param in "${file}": "${seg}" must be [name], [[name]], [...name], or a (group) folder`,
|
|
277
457
|
)
|
|
278
458
|
}
|
|
279
|
-
|
|
459
|
+
push(`:${match[1]}`)
|
|
280
460
|
} else {
|
|
281
|
-
|
|
461
|
+
push(seg)
|
|
282
462
|
}
|
|
463
|
+
mark()
|
|
283
464
|
}
|
|
284
|
-
return combos.map((c) => `/${c.join("/")}
|
|
465
|
+
return combos.map((c) => ({ pattern: `/${c.segs.join("/")}`, depths: c.depths }))
|
|
285
466
|
}
|
|
286
467
|
|
|
287
468
|
/**
|
|
288
|
-
* The **canonical** single pattern for a route file
|
|
469
|
+
* The **canonical** single pattern for a route file - all optional segments present. A file with no
|
|
289
470
|
* optionals yields its one pattern. Use {@link filePathToPatterns} to get every pattern (optionals
|
|
290
471
|
* expand the set).
|
|
291
472
|
*/
|
|
@@ -295,6 +476,26 @@ export function filePathToPattern(file: string): string {
|
|
|
295
476
|
}
|
|
296
477
|
|
|
297
478
|
/** The dir chain from root → the file's own dir, e.g. "a/b/p.tsx" → ["", "a", "a/b"]. */
|
|
479
|
+
/**
|
|
480
|
+
* Param names in the first `count` segments of a pattern - the params a layout at that depth owns.
|
|
481
|
+
*
|
|
482
|
+
* This is the whole scoping mechanism: a layout wraps a URL prefix, so the params inside that prefix
|
|
483
|
+
* are the ones it may read, and the ones whose change should re-run its loader. Everything deeper
|
|
484
|
+
* belongs to routes below it and must not invalidate it.
|
|
485
|
+
*/
|
|
486
|
+
function paramsInPrefix(pattern: string, count: number): string[] {
|
|
487
|
+
if (count === 0) return []
|
|
488
|
+
const names: string[] = []
|
|
489
|
+
const segments = pattern === "/" ? [] : pattern.slice(1).split("/")
|
|
490
|
+
for (const segment of segments.slice(0, count)) {
|
|
491
|
+
// Mirrors the router's own grammar: `:name`, `*name`, and a mixed segment's embedded `:name`s.
|
|
492
|
+
for (const match of segment.matchAll(/[:*]([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
493
|
+
names.push(match[1] as string)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return names
|
|
497
|
+
}
|
|
498
|
+
|
|
298
499
|
const ancestorDirs = (file: string): string[] => {
|
|
299
500
|
const dirs = [""]
|
|
300
501
|
const dir = dirOf(file)
|
|
@@ -309,7 +510,7 @@ const ancestorDirs = (file: string): string[] => {
|
|
|
309
510
|
|
|
310
511
|
/**
|
|
311
512
|
* Build a manifest from route file paths (relative to the routes dir) + an `importer` that
|
|
312
|
-
* turns a path into a lazy module loader. Pure
|
|
513
|
+
* turns a path into a lazy module loader. Pure - no fs. Throws at boot (the loud-and-early
|
|
313
514
|
* RouteConfigError ethos) on duplicate patterns. `_layout`/`_404`/`_error` files are special; other
|
|
314
515
|
* `_`-prefixed files are ignored (private/colocated, never routed).
|
|
315
516
|
*/
|
|
@@ -322,6 +523,7 @@ export function buildManifest(
|
|
|
322
523
|
const errorDirs = new Set<string>()
|
|
323
524
|
const errors: Record<string, LayoutEntry> = {}
|
|
324
525
|
let notFound: LayoutEntry | undefined
|
|
526
|
+
const statusPages: Record<string, LayoutEntry> = {}
|
|
325
527
|
const routeFiles: string[] = []
|
|
326
528
|
|
|
327
529
|
for (const file of files) {
|
|
@@ -336,6 +538,11 @@ export function buildManifest(
|
|
|
336
538
|
errors[errorIdFor(dir)] = { file, load: importer(file) }
|
|
337
539
|
} else if (stem === "_404") {
|
|
338
540
|
notFound = { file, load: importer(file) }
|
|
541
|
+
} else if (STATUS_PAGE.test(stem) && dirOf(file) === "") {
|
|
542
|
+
// `_410.tsx`, `_451.tsx`, … at the routes root. Root-only: unlike `_error`, these are not
|
|
543
|
+
// resolved per segment - a terminal status is a property of the outcome, not of where in the
|
|
544
|
+
// tree the route lives, and a per-segment variant would need a precedence rule nobody asked for.
|
|
545
|
+
statusPages[stem.slice(1)] = { file, load: importer(file) }
|
|
339
546
|
} else if (!stem.startsWith("_")) {
|
|
340
547
|
routeFiles.push(file)
|
|
341
548
|
}
|
|
@@ -345,13 +552,14 @@ export function buildManifest(
|
|
|
345
552
|
const routes: RouteEntry[] = []
|
|
346
553
|
for (const file of routeFiles) {
|
|
347
554
|
const dirs = ancestorDirs(file)
|
|
348
|
-
const
|
|
555
|
+
const layoutDirsForFile = dirs.filter((dir) => layoutDirs.has(dir))
|
|
556
|
+
const layoutIds = layoutDirsForFile.map(layoutIdFor)
|
|
349
557
|
const errorIds = dirs.filter((dir) => errorDirs.has(dir)).map(errorIdFor)
|
|
350
558
|
const id = stripExt(file)
|
|
351
559
|
const load = importer(file) // one lazy loader per file, shared by its (possibly expanded) patterns
|
|
352
560
|
// An optional `[[x]]` segment expands a file into multiple patterns, all pointing at the same
|
|
353
561
|
// module (same id/load/layout chain). Distinct patterns ⇒ no match ambiguity (different lengths).
|
|
354
|
-
for (const pattern of
|
|
562
|
+
for (const { pattern, depths } of filePathToRoutes(file)) {
|
|
355
563
|
const existing = byPattern.get(pattern)
|
|
356
564
|
if (existing !== undefined) {
|
|
357
565
|
throw new Error(
|
|
@@ -359,11 +567,19 @@ export function buildManifest(
|
|
|
359
567
|
)
|
|
360
568
|
}
|
|
361
569
|
byPattern.set(pattern, file)
|
|
362
|
-
|
|
570
|
+
const layoutParams = layoutDirsForFile.map((dir) =>
|
|
571
|
+
paramsInPrefix(pattern, depths[dir === "" ? 0 : dir.split("/").length] ?? 0),
|
|
572
|
+
)
|
|
573
|
+
routes.push({ id, pattern, layoutIds, layoutParams, errorIds, file, load })
|
|
363
574
|
}
|
|
364
575
|
}
|
|
365
576
|
|
|
366
|
-
const base: Manifest = {
|
|
577
|
+
const base: Manifest = {
|
|
578
|
+
routes,
|
|
579
|
+
layouts,
|
|
580
|
+
errors,
|
|
581
|
+
...(Object.keys(statusPages).length > 0 ? { statusPages } : {}),
|
|
582
|
+
}
|
|
367
583
|
return notFound === undefined ? base : { ...base, notFound }
|
|
368
584
|
}
|
|
369
585
|
|
|
@@ -402,7 +618,7 @@ export function fillRoutePattern(
|
|
|
402
618
|
/** The static-routing facts a server needs from the route modules: which concrete paths are
|
|
403
619
|
* prerendered, plus each dynamic route's `getStaticPaths` fallback policy. */
|
|
404
620
|
export interface StaticRoutes {
|
|
405
|
-
/** Concrete prerendered paths
|
|
621
|
+
/** Concrete prerendered paths - static `prerender` routes + each `getStaticPaths` entry. */
|
|
406
622
|
readonly paths: string[]
|
|
407
623
|
/** Per dynamic route pattern, its `getStaticPaths` `fallback` (`"ssr"` default). `createWebApp` uses
|
|
408
624
|
* `"404"` to reject an unlisted path under that route at runtime (the path simply doesn't exist). */
|
|
@@ -410,7 +626,7 @@ export interface StaticRoutes {
|
|
|
410
626
|
}
|
|
411
627
|
|
|
412
628
|
/**
|
|
413
|
-
* Enumerate the static-routing facts `prerenderRoutes` would produce
|
|
629
|
+
* Enumerate the static-routing facts `prerenderRoutes` would produce - static routes opted in via
|
|
414
630
|
* `export const prerender = true`, each `getStaticPaths` entry of a dynamic route, and each dynamic
|
|
415
631
|
* route's `fallback` policy. Pure (no rendering), so a server can compute what to hand `createWebApp`
|
|
416
632
|
* (the prerendered set for the client's static-`_data.json` soft-nav + the fallback map for
|