@pyreon/zero 0.15.0 → 0.18.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 (52) hide show
  1. package/lib/{api-routes-DANluJic.js → api-routes-Ci0kVmM4.js} +2 -2
  2. package/lib/client.js +4 -1
  3. package/lib/env.js +6 -6
  4. package/lib/font.js +3 -3
  5. package/lib/{fs-router-ZebyutPa.js → fs-router-MewHc5SB.js} +25 -30
  6. package/lib/i18n-routing.js +112 -1
  7. package/lib/image.js +140 -58
  8. package/lib/index.js +252 -82
  9. package/lib/og-image.js +5 -5
  10. package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
  11. package/lib/script.js +114 -25
  12. package/lib/seo.js +186 -15
  13. package/lib/server.js +274 -564
  14. package/lib/types/config.d.ts +307 -3
  15. package/lib/types/env.d.ts +2 -2
  16. package/lib/types/i18n-routing.d.ts +193 -2
  17. package/lib/types/image.d.ts +105 -5
  18. package/lib/types/index.d.ts +666 -182
  19. package/lib/types/script.d.ts +78 -6
  20. package/lib/types/seo.d.ts +128 -4
  21. package/lib/types/server.d.ts +607 -72
  22. package/lib/vite-plugin-y0NmCLJA.js +2476 -0
  23. package/package.json +11 -10
  24. package/src/adapters/bun.ts +20 -1
  25. package/src/adapters/cloudflare.ts +78 -1
  26. package/src/adapters/index.ts +25 -3
  27. package/src/adapters/netlify.ts +63 -1
  28. package/src/adapters/node.ts +25 -1
  29. package/src/adapters/static.ts +26 -1
  30. package/src/adapters/validate.ts +8 -1
  31. package/src/adapters/vercel.ts +76 -1
  32. package/src/adapters/warn-missing-env.ts +49 -0
  33. package/src/app.ts +14 -0
  34. package/src/client.ts +18 -0
  35. package/src/entry-server.ts +55 -5
  36. package/src/env.ts +7 -7
  37. package/src/font.ts +3 -3
  38. package/src/fs-router.ts +72 -3
  39. package/src/i18n-routing.ts +246 -12
  40. package/src/image.tsx +242 -91
  41. package/src/index.ts +4 -4
  42. package/src/isr.ts +24 -6
  43. package/src/manifest.ts +675 -0
  44. package/src/og-image.ts +5 -5
  45. package/src/script.tsx +159 -36
  46. package/src/seo.ts +346 -15
  47. package/src/server.ts +10 -2
  48. package/src/ssg-plugin.ts +1211 -54
  49. package/src/types.ts +333 -10
  50. package/src/vercel-revalidate-handler.ts +204 -0
  51. package/src/vite-plugin.ts +171 -41
  52. package/lib/vite-plugin-E4BHYvYW.js +0 -855
@@ -1,5 +1,18 @@
1
1
  import { Middleware } from "@pyreon/server";
2
-
2
+ //#region src/i18n-routing.d.ts
3
+ interface I18nRoutingConfig {
4
+ /** Supported locales. e.g. ["en", "de", "cs"] */
5
+ locales: string[];
6
+ /** Default locale — served without prefix (/ instead of /en/). */
7
+ defaultLocale: string;
8
+ /** Redirect root to detected locale. Default: true */
9
+ detectLocale?: boolean;
10
+ /** Cookie name to persist locale preference. Default: "locale" */
11
+ cookieName?: string;
12
+ /** URL strategy. Default: "prefix-except-default" */
13
+ strategy?: 'prefix' | 'prefix-except-default';
14
+ }
15
+ //#endregion
3
16
  //#region src/types.d.ts
4
17
  type RenderMode = 'ssr' | 'ssg' | 'spa' | 'isr';
5
18
  interface ISRConfig {
@@ -12,6 +25,42 @@ interface ISRConfig {
12
25
  * space (e.g. `/user/:id` where `:id` is free-form).
13
26
  */
14
27
  maxEntries?: number;
28
+ /**
29
+ * Cache-key derivation function. The default keys cache entries by
30
+ * `url.pathname` ONLY — query strings, cookies, and headers are
31
+ * stripped.
32
+ *
33
+ * **⚠️ Auth-gated incompatibility.** The default behavior is
34
+ * unsafe for request-dependent loaders. A loader that reads
35
+ * `request.headers.get('cookie')` to gate auth will render ONCE
36
+ * with the first user's cookie, then serve that HTML to every
37
+ * subsequent user. To use ISR with personalized / auth-gated
38
+ * pages, supply a `cacheKey` that varies on the auth identifier
39
+ * (session cookie, user-id header, etc.), OR don't use ISR for
40
+ * such routes — use SSR instead.
41
+ *
42
+ * @example
43
+ * // Vary cache by session cookie:
44
+ * isr: {
45
+ * revalidate: 60,
46
+ * cacheKey: (req) => {
47
+ * const url = new URL(req.url)
48
+ * const session = req.headers.get('cookie')?.match(/session=([^;]+)/)?.[1] ?? 'anon'
49
+ * return `${url.pathname}::${session}`
50
+ * },
51
+ * }
52
+ *
53
+ * @example
54
+ * // Vary by a query parameter:
55
+ * isr: {
56
+ * revalidate: 60,
57
+ * cacheKey: (req) => {
58
+ * const url = new URL(req.url)
59
+ * return `${url.pathname}?sort=${url.searchParams.get('sort') ?? ''}`
60
+ * },
61
+ * }
62
+ */
63
+ cacheKey?: (req: Request) => string;
15
64
  }
16
65
  interface ZeroConfig {
17
66
  /** Default rendering mode. Default: "ssr" */
@@ -25,18 +74,273 @@ interface ZeroConfig {
25
74
  /** SSG options — only used when mode is "ssg". */
26
75
  ssg?: {
27
76
  /** Paths to prerender (or function returning paths). */paths?: string[] | (() => string[] | Promise<string[]>);
77
+ /**
78
+ * Auto-emit `dist/404.html` from the route tree's `_404.tsx` /
79
+ * `_not-found.tsx` convention. fs-router already wires `_404.tsx` as
80
+ * `notFoundComponent` on its parent layout route; the SSG plugin walks
81
+ * the tree, picks up the first one, renders it through the same SSR
82
+ * pipeline as regular paths (so styler CSS / @pyreon/head metadata land
83
+ * correctly), and writes the result to `dist/404.html`. Static hosts
84
+ * (Netlify, Cloudflare Pages, GitHub Pages, S3+CloudFront) serve this
85
+ * file automatically for unmatched URLs. Default: `true`. Set to
86
+ * `false` to opt out — the route tree is left alone.
87
+ */
88
+ emit404?: boolean;
89
+ /**
90
+ * When a route loader throws `redirect('/target')` during SSG, write
91
+ * a `dist/_redirects` file (Netlify / Cloudflare Pages convention)
92
+ * AND a `dist/_redirects.json` (Vercel convention) listing every
93
+ * redirected source path → target. Static hosts pick whichever
94
+ * format their platform supports automatically. The redirected
95
+ * path's HTML file is NOT emitted — the redirect is the response.
96
+ *
97
+ * Without this option, redirect-throwing loaders land in
98
+ * `errors[]` and the path silently disappears from the build —
99
+ * the user sees no output for `/old` AND no warning that the
100
+ * loader ran a redirect. Default: `true`. Set to `false` to
101
+ * restore the pre-PR-B behaviour (redirects treated as errors).
102
+ */
103
+ emitRedirects?: boolean;
104
+ /**
105
+ * Additionally emit a static HTML file at the source path with a
106
+ * `<meta http-equiv="refresh">` redirect — for adapters / hosts
107
+ * that don't read `_redirects` (plain S3, GitHub Pages, simple
108
+ * file servers). The meta-refresh fallback works on any HTTP
109
+ * server that serves static files.
110
+ *
111
+ * - `'none'` (default): only `_redirects` / `_redirects.json` are
112
+ * emitted; no per-redirect HTML file.
113
+ * - `'meta-refresh'`: emit `dist/<source>/index.html` containing
114
+ * `<meta http-equiv="refresh" content="0; url=<target>">` plus
115
+ * a canonical link tag for SEO. Status code information is
116
+ * lost (meta-refresh has no status equivalent), so 301/302/307/
117
+ * 308 all collapse to "client-side refresh".
118
+ */
119
+ redirectsAsHtml?: 'none' | 'meta-refresh';
120
+ /**
121
+ * Callback invoked when a path's render throws (loader-throw that
122
+ * isn't a `redirect()`, render exception, anything that lands in the
123
+ * `errors[]` collection). Returns either:
124
+ * - `string` → written as the path's HTML in place of the failed
125
+ * render. Use this to emit a per-path fallback page (e.g. a generic
126
+ * "this content is temporarily unavailable" template) so static
127
+ * hosts have something to serve at that URL instead of 404'ing.
128
+ * - `null` → skip; the path produces no HTML output. The error
129
+ * stays in `errors[]` for the post-build summary.
130
+ *
131
+ * The callback runs ONCE per failed path. Async callbacks are
132
+ * awaited. If the callback itself throws, the throw is captured as
133
+ * a separate error entry and the path is skipped (no fallback HTML).
134
+ * Default: `undefined` — failed paths just land in `errors[]`.
135
+ *
136
+ * @example
137
+ * ssg: {
138
+ * onPathError: async (path, error) => {
139
+ * console.error(\`SSG render failed for \${path}:\`, error)
140
+ * return \`<!DOCTYPE html><html><body><h1>Page unavailable</h1></body></html>\`
141
+ * },
142
+ * }
143
+ */
144
+ onPathError?: (path: string, error: unknown) => string | null | Promise<string | null>;
145
+ /**
146
+ * When `'json'` (default), write `dist/_pyreon-ssg-errors.json` after
147
+ * the render loop summarising every error encountered (path traversal,
148
+ * timeout, render exception, getStaticPaths throw, fallback callback
149
+ * throw). Each entry has `{ path, message, name, stack }`. The file
150
+ * is ONLY written when `errors.length > 0` — successful builds don't
151
+ * leak an empty manifest. Reading it lets CI gate on render failures
152
+ * without parsing console output (e.g.
153
+ * `cat dist/_pyreon-ssg-errors.json | jq '.errors | length' | grep -q 0`).
154
+ *
155
+ * Set to `'none'` to opt out entirely — errors stay in console-only,
156
+ * matching pre-PR-G behaviour.
157
+ */
158
+ errorArtifact?: 'json' | 'none';
159
+ /**
160
+ * Maximum number of paths rendered in parallel during the SSG closeBundle
161
+ * loop. Default: `4` — a sensible balance between speedup and the risk
162
+ * of exhausting downstream resources (DB connection pools, fetch
163
+ * rate-limits) inside loaders. Set to `1` to render fully sequentially
164
+ * (the pre-PR-D behaviour). Set to a higher value for faster builds
165
+ * on CI / multi-core hosts; the practical ceiling is the number of
166
+ * loader-side concurrent connections your app's data layer tolerates.
167
+ *
168
+ * The render-error pipeline (`onPathError` callback, `errors[]`
169
+ * collection, `_pyreon-ssg-errors.json` artifact) is unchanged —
170
+ * concurrency only affects how many paths are in flight at once,
171
+ * not how their successes / failures are recorded.
172
+ *
173
+ * @example
174
+ * ssg: {
175
+ * concurrency: 8, // Faster builds for static-content sites
176
+ * }
177
+ */
178
+ concurrency?: number;
179
+ /**
180
+ * Per-path progress callback. Invoked once per path AFTER its render
181
+ * settles (success, redirect, OR failure) — never during in-flight
182
+ * renders. Receives `{ completed, total, currentPath, elapsed }`
183
+ * where:
184
+ * - `completed` is the count of paths whose render has settled (1-indexed)
185
+ * - `total` is the full path count from `resolvePaths()`
186
+ * - `currentPath` is the path that just settled
187
+ * - `elapsed` is wall-clock ms since the loop started
188
+ *
189
+ * Use cases: build-tool progress bars (Vite picks up stdout), CI
190
+ * heartbeat lines on long builds (10k-path sites take minutes —
191
+ * silent stretches look hung), build-time perf instrumentation.
192
+ *
193
+ * Async callbacks are awaited before the next path's progress fires,
194
+ * so a slow callback can serialize progress reporting (it does NOT
195
+ * gate the worker pool — paths keep rendering in parallel; only
196
+ * the progress callbacks themselves are serialized). Throws are
197
+ * captured into `errors[]` with the path suffix `(onProgress)` so
198
+ * a buggy callback can't take down the build.
199
+ *
200
+ * @example
201
+ * ssg: {
202
+ * onProgress: ({ completed, total, currentPath, elapsed }) => {
203
+ * console.log(`[${completed}/${total}] ${currentPath} (${elapsed}ms)`)
204
+ * },
205
+ * }
206
+ */
207
+ onProgress?: (info: {
208
+ completed: number;
209
+ total: number;
210
+ currentPath: string;
211
+ elapsed: number;
212
+ }) => void | Promise<void>;
213
+ /**
214
+ * Route-level code splitting in SSG mode. Default `true`.
215
+ *
216
+ * When `true` (default), each route file becomes its own dynamic-import
217
+ * chunk via `lazy(() => import("..."))` — only the route the user
218
+ * lands on plus its dependencies ship in the initial bundle, the
219
+ * rest fetch on navigation. Matches the SSR/SPA-mode behaviour zero
220
+ * has always had; brings parity to SSG.
221
+ *
222
+ * When `false`, every route is bundled statically into the main
223
+ * client chunk (the pre-2026-Q3 SSG behaviour). Useful for tiny
224
+ * sites (2-5 pages) where the single-chunk-then-instant-nav trade
225
+ * is preferable — the chunk-fetch cost on navigation is gone, and
226
+ * the marginal bytes are negligible.
227
+ *
228
+ * Crossover point: ~5-8 routes. Below that, single-chunk is fine.
229
+ * Above that, lazy() shrinks the initial bundle by a meaningful
230
+ * amount (a 50-route docs site might drop from 200 KB to 80 KB on
231
+ * first paint).
232
+ *
233
+ * Underlying mechanism is the same 3-tier generator zero already
234
+ * uses for SSR/SPA mode (`fs-router.ts:generateRouteEntry`): lazy
235
+ * component + inlined metadata when possible, lazy + lazy-thunked
236
+ * function exports when not, namespace-import fallback for cases
237
+ * the literal-extractor can't reach.
238
+ *
239
+ * @example
240
+ * ssg: {
241
+ * splitChunks: false, // bundle-everything for a 3-page marketing site
242
+ * }
243
+ */
244
+ splitChunks?: boolean;
28
245
  };
29
246
  /** ISR config — only used when mode is "isr". */
30
247
  isr?: ISRConfig;
31
- /** Deploy adapter. Default: "node" */
32
- adapter?: 'node' | 'bun' | 'static' | 'vercel' | 'cloudflare' | 'netlify';
248
+ /**
249
+ * Deploy adapter. Default: `"node"`.
250
+ *
251
+ * Accepts either a built-in adapter name (string) OR a constructed
252
+ * `Adapter` instance (e.g. `vercelAdapter()`). The scaffolded templates
253
+ * emit the instance form (`adapter: vercelAdapter()`) by convention.
254
+ * `resolveAdapter` (see `adapters/index.ts`) accepts both shapes —
255
+ * strings go through a switch lookup, instances pass through
256
+ * unchanged.
257
+ */
258
+ adapter?: 'node' | 'bun' | 'static' | 'vercel' | 'cloudflare' | 'netlify' | Adapter;
33
259
  /** Base URL path. Default: "/" */
34
260
  base?: string;
261
+ /**
262
+ * i18n routing — locale-prefixed route variants generated at build time
263
+ * (PR H of the SSG roadmap). When set, every `FileRoute` is fanned into
264
+ * per-locale duplicates by `expandRoutesForLocales` from
265
+ * `@pyreon/zero`. Independent from the `i18nRouting()` Vite plugin
266
+ * (which only handles request-time locale detection); both can be used
267
+ * together. See `expandRoutesForLocales` JSDoc for strategy semantics.
268
+ */
269
+ i18n?: I18nRoutingConfig;
35
270
  /** App-level middleware applied to all routes. */
36
271
  middleware?: Middleware[];
37
272
  /** Server port for dev/preview. Default: 3000 */
38
273
  port?: number;
39
274
  }
275
+ interface Adapter {
276
+ name: string;
277
+ /** Build the production server/output for this adapter. */
278
+ build(options: AdapterBuildOptions): Promise<void>;
279
+ /**
280
+ * Revalidate a prerendered path on the deploy platform's ISR layer
281
+ * (PR I — build-time ISR). Called by user code (webhook handlers,
282
+ * cron jobs, CMS triggers, etc.) to trigger a rebuild-on-stale for
283
+ * the named path. Optional — adapters without platform ISR support
284
+ * (static, node, bun) implement a no-op. Returns `{ regenerated:
285
+ * boolean }` so user code can branch on whether the platform actually
286
+ * accepted the revalidation request.
287
+ *
288
+ * Distinct from runtime ISR (`mode: 'isr'`, on-demand LRU caching in
289
+ * `@pyreon/zero/server`'s `createISRHandler`). Build-time ISR is
290
+ * static prerender + platform-driven rebuild-on-stale; runtime ISR is
291
+ * SSR-cached-with-TTL. They can coexist.
292
+ *
293
+ * Per-route `revalidate` metadata flows from `export const revalidate
294
+ * = 60` in route files into a `dist/_pyreon-revalidate.json` manifest
295
+ * the adapter reads at deploy time. Adapters use that manifest to
296
+ * configure platform ISR (Vercel `output/config.json`, Cloudflare
297
+ * Cache API rules, Netlify revalidation headers).
298
+ */
299
+ revalidate?(path: string): Promise<AdapterRevalidateResult>;
300
+ }
301
+ /**
302
+ * Result of `Adapter.revalidate(path)`. `regenerated: false` means the
303
+ * adapter does not support platform ISR (no-op fallback) OR the
304
+ * platform rejected the request. Adapters that throw on platform-API
305
+ * failure should let it propagate so user code can handle the rejection.
306
+ */
307
+ interface AdapterRevalidateResult {
308
+ regenerated: boolean;
309
+ }
310
+ /**
311
+ * Inputs the build pipeline passes to an adapter's `build()` method.
312
+ *
313
+ * The `kind` field discriminates the two shapes. **SSR mode** (`'ssr'`)
314
+ * carries `serverEntry` + `clientOutDir` so adapters can wrap the user's
315
+ * server bundle as a serverless function. **SSG mode** (`'ssg'`) carries
316
+ * only `outDir` (which IS the rendered dist/) — no serverEntry exists
317
+ * because every page is already prerendered. SSG-mode adapters write
318
+ * platform-specific routing config so the host knows the deploy is
319
+ * fully-static (no function invocation per request).
320
+ *
321
+ * Pre-PR-J this was a single SSR-shaped struct; the SSG path had no way
322
+ * to invoke `adapter.build()` because it couldn't supply `serverEntry`.
323
+ * Adding `kind` (with TS-narrowing per branch) lets `ssgPlugin`
324
+ * `closeBundle` call `adapter.build({ kind: 'ssg', outDir, config })`
325
+ * cleanly, AND keeps the SSR-mode adapter implementations unchanged.
326
+ */
327
+ type AdapterBuildOptions = {
328
+ kind: 'ssr'; /** Path to the built server entry. */
329
+ serverEntry: string; /** Path to the client build output. */
330
+ clientOutDir: string; /** Final output directory. */
331
+ outDir: string;
332
+ config: ZeroConfig;
333
+ } | {
334
+ kind: 'ssg';
335
+ /**
336
+ * The rendered dist directory. For SSG, this directory IS the
337
+ * publishable output — adapters write platform-specific routing
338
+ * config alongside (e.g. `.vercel/output/config.json`,
339
+ * `_routes.json`, `netlify.toml`) but generally don't move files.
340
+ */
341
+ outDir: string;
342
+ config: ZeroConfig;
343
+ };
40
344
  //#endregion
41
345
  //#region src/config.d.ts
42
346
  /**
@@ -81,7 +81,7 @@ type InferEnvSchema<T> = { [K in keyof T]: InferEntry<T[K]> };
81
81
  * })
82
82
  * ```
83
83
  */
84
- declare function validateEnv<T extends Record<string, SchemaEntry>>(schema: T, source?: Record<string, string | undefined>): InferEnvSchema<T>;
84
+ declare function validateEnv<T extends Record<string, SchemaEntry>>(envSchema: T, source?: Record<string, string | undefined>): InferEnvSchema<T>;
85
85
  /**
86
86
  * Extract public environment variables (prefixed with `ZERO_PUBLIC_`).
87
87
  *
@@ -95,7 +95,7 @@ declare function validateEnv<T extends Record<string, SchemaEntry>>(schema: T, s
95
95
  * ```
96
96
  */
97
97
  declare function publicEnv(): Record<string, string>;
98
- declare function publicEnv<T extends Record<string, SchemaEntry>>(schema: T): InferEnvSchema<T>;
98
+ declare function publicEnv<T extends Record<string, SchemaEntry>>(envSchema: T): InferEnvSchema<T>;
99
99
  /**
100
100
  * Create an env validator from a custom parse function.
101
101
  * Use this to integrate any schema library (Zod, Valibot, ArkType, etc.).
@@ -1,7 +1,133 @@
1
1
  import * as _$_pyreon_core0 from "@pyreon/core";
2
2
  import * as _$_pyreon_reactivity0 from "@pyreon/reactivity";
3
3
  import { Plugin } from "vite";
4
-
4
+ //#region src/types.d.ts
5
+ type RenderMode = 'ssr' | 'ssg' | 'spa' | 'isr';
6
+ /**
7
+ * Which optional metadata exports a route file declares.
8
+ * Detected at scan time by parsing the file source. The code generator
9
+ * uses this to skip emitting `import * as mod` for routes that only
10
+ * export `default`, eliminating the dual-import collision with `lazy()`
11
+ * and silencing `IMPORT_IS_UNDEFINED` warnings from Rolldown.
12
+ */
13
+ interface RouteFileExports {
14
+ /** Has `export const loader` or `export function loader` */
15
+ hasLoader: boolean;
16
+ /** Has `export const guard` or `export function guard` */
17
+ hasGuard: boolean;
18
+ /** Has `export const meta` */
19
+ hasMeta: boolean;
20
+ /** Has `export const renderMode` */
21
+ hasRenderMode: boolean;
22
+ /** Has `export const error` (custom per-route error component) */
23
+ hasError: boolean;
24
+ /** Has `export const middleware` */
25
+ hasMiddleware: boolean;
26
+ /**
27
+ * Has `export const loaderKey` or `export function loaderKey`. When present,
28
+ * the route generator wires it as the `loaderKey` field on the route record,
29
+ * which controls cache identity for `_loaderCache`. Useful for auth-gate
30
+ * loaders that should invalidate when the session cookie changes — read
31
+ * `document.cookie` (CSR) or `ctx.request.headers.get('cookie')` (SSR) and
32
+ * derive a key from session identity. Default cache key is `path + params`,
33
+ * which doesn't see cookie changes.
34
+ */
35
+ hasLoaderKey: boolean;
36
+ /**
37
+ * Has `export const gcTime` (number, in ms). When present, the route generator
38
+ * inlines it on the route record. `gcTime: 0` disables caching entirely —
39
+ * the loader runs on every navigation. Useful for auth-gate loaders that
40
+ * must validate session on every navigation rather than serve stale data.
41
+ */
42
+ hasGcTime: boolean;
43
+ /**
44
+ * Has `export function getStaticPaths` or `export const getStaticPaths`.
45
+ * Used at SSG build time to enumerate concrete values for dynamic routes
46
+ * (`/posts/[id].tsx` → `[/posts/1, /posts/2, …]`). The function returns
47
+ * `Array<{ params: Record<string, string> }>`. Mirrors Astro's per-route
48
+ * convention. Without it, dynamic routes are silently skipped during SSG
49
+ * auto-detect — the user must hand-list every value in `ssg.paths`.
50
+ */
51
+ hasGetStaticPaths: boolean;
52
+ /**
53
+ * Has `export const revalidate` (number, in seconds, or `false` for
54
+ * never-revalidate). PR I — build-time ISR. The SSG plugin emits a
55
+ * `dist/_pyreon-revalidate.json` manifest mapping `{ path: revalidate }`
56
+ * which the deploy adapter (Vercel / Cloudflare / Netlify) consumes
57
+ * to wire platform-specific ISR rebuild-on-stale. The route generator
58
+ * does NOT inline `revalidate` onto the route record — it's a
59
+ * build-time-only concern that never reaches the runtime router.
60
+ */
61
+ hasRevalidate: boolean;
62
+ /**
63
+ * Raw text of the `export const meta = …` initializer, captured as a
64
+ * literal expression. When present, the route generator inlines this
65
+ * value directly into the generated routes module instead of importing
66
+ * it from the route file — which means the route file can be lazy()'d
67
+ * without forcing the entire dependency tree into the main bundle.
68
+ *
69
+ * Only set when the meta export is a top-level `export const meta = { … }`
70
+ * literal that can be extracted via balanced-brace scanning. Anything
71
+ * fancier (computed values, function calls, references to other
72
+ * declarations) leaves this undefined and falls back to a static module
73
+ * import.
74
+ */
75
+ metaLiteral?: string;
76
+ /**
77
+ * Raw text of the `export const renderMode = …` initializer, captured
78
+ * as a literal expression. Same inlining strategy as `metaLiteral`.
79
+ */
80
+ renderModeLiteral?: string;
81
+ /**
82
+ * Raw text of the `export const revalidate = …` initializer (e.g.
83
+ * `'60'`, `'false'`, `'3600'`). Captured at scan time so the SSG
84
+ * plugin can read the value to emit the build-time ISR manifest
85
+ * WITHOUT loading the route module — which is critical because the
86
+ * manifest is emitted from the synthetic SSR build's outer plugin
87
+ * context, where evaluating route modules would re-trigger the
88
+ * recursive sub-build env-flag guard.
89
+ *
90
+ * Only set when the revalidate export is a top-level
91
+ * `export const revalidate = <numeric|boolean literal>` that passes
92
+ * `isPureLiteral`. Anything else (function calls, references to
93
+ * other declarations) leaves this undefined and the manifest falls
94
+ * back to omitting the entry.
95
+ */
96
+ revalidateLiteral?: string;
97
+ }
98
+ /** Internal representation of a file-system route before conversion to RouteRecord. */
99
+ interface FileRoute {
100
+ /** File path relative to routes dir (e.g. "users/[id].tsx") */
101
+ filePath: string;
102
+ /** Parsed URL path pattern (e.g. "/users/:id") */
103
+ urlPath: string;
104
+ /** Directory path for grouping (e.g. "users" or "" for root) */
105
+ dirPath: string;
106
+ /** Route segment depth for nesting. */
107
+ depth: number;
108
+ /** Whether this is a layout file. */
109
+ isLayout: boolean;
110
+ /** Whether this is an error boundary file. */
111
+ isError: boolean;
112
+ /** Whether this is a loading fallback file. */
113
+ isLoading: boolean;
114
+ /** Whether this is a not-found (404) file. */
115
+ isNotFound: boolean;
116
+ /** Whether this is a catch-all route. */
117
+ isCatchAll: boolean;
118
+ /** Resolved rendering mode. */
119
+ renderMode: RenderMode;
120
+ /**
121
+ * Detected optional exports from the file source.
122
+ * When undefined, the generator treats the file as having no metadata
123
+ * exports and emits the optimal `lazy()` shape (one dynamic import,
124
+ * no static metadata wiring). When provided, the generator emits a
125
+ * single namespace import for files with metadata or `lazy()` for
126
+ * files with only a default export.
127
+ */
128
+ exports?: RouteFileExports;
129
+ }
130
+ //#endregion
5
131
  //#region src/i18n-routing.d.ts
6
132
  interface I18nRoutingConfig {
7
133
  /** Supported locales. e.g. ["en", "de", "cs"] */
@@ -46,6 +172,71 @@ declare function extractLocaleFromPath(path: string, locales: string[], defaultL
46
172
  * Build a localized path.
47
173
  */
48
174
  declare function buildLocalePath(path: string, locale: string, defaultLocale: string, strategy: 'prefix' | 'prefix-except-default'): string;
175
+ /**
176
+ * Fan a `FileRoute[]` into per-locale duplicates so the file-system router
177
+ * knows about every localized URL pattern at build time. PR H — was the
178
+ * missing half of the i18n story before this PR (the `i18nRouting()` Vite
179
+ * plugin only handled request-time locale detection; routes themselves
180
+ * were never duplicated, so static-host SSG outputs and SSR matching had
181
+ * no `/de/about` / `/cs/about` records to render against).
182
+ *
183
+ * Strategy semantics:
184
+ *
185
+ * - **`prefix-except-default`** (default): the default locale's routes
186
+ * keep their original `urlPath` unchanged (`/about` stays `/about`); all
187
+ * non-default locales get a prefix (`/de/about`, `/cs/about`). Best for
188
+ * SEO-on-default-locale apps — search engines see canonical URLs at
189
+ * `/about` while non-default speakers get explicit prefixes.
190
+ *
191
+ * - **`prefix`**: every locale gets its own prefix, including the default
192
+ * (`/en/about`, `/de/about`, `/cs/about`). Root `/` becomes `/en` /
193
+ * `/de` / `/cs`. Better when no locale is "primary" — every URL
194
+ * self-identifies its locale.
195
+ *
196
+ * Layouts, error boundaries, loading components, and 404 pages duplicate
197
+ * along with their pages — same source file (same `filePath`), new
198
+ * locale-prefixed `urlPath` / `dirPath` / `depth`. The route tree built
199
+ * from the expanded array therefore has one fully-formed subtree per
200
+ * locale, so layout matching, dynamic params (`[id]` → `:id`), and
201
+ * catch-all routes (`[...slug]` → `:slug*`) all compose naturally with
202
+ * the locale prefix — no special cases.
203
+ *
204
+ * `getStaticPaths` composition (for SSG): each duplicate route inherits
205
+ * the same `exports.getStaticPaths`. The SSG plugin's `expandUrlPattern`
206
+ * step then expands `/blog/[slug]` × `[en, de]` × `getStaticPaths()
207
+ * → ['a', 'b']` into `/blog/a`, `/blog/b`, `/de/blog/a`, `/de/blog/b`
208
+ * (or all six prefixed forms under `'prefix'` strategy). Cardinality
209
+ * compounds, which is by design — `ssg.concurrency` (PR D) limits
210
+ * in-flight renders independent of route count.
211
+ *
212
+ * No-op when `config.locales` is empty or contains only the default
213
+ * locale (prefix-except-default strategy with no other locales) — returns
214
+ * the input array unchanged. Always return a fresh array on duplication
215
+ * so callers don't accidentally mutate cached input.
216
+ *
217
+ * Reference: the helper is called from `vite-plugin.ts`'s virtual route
218
+ * module load AND `ssg-plugin.ts`'s pre-render path expansion. Tested in
219
+ * isolation — duplication is a pure transform on FileRoute[] with no
220
+ * filesystem or network side effects.
221
+ */
222
+ declare function expandRoutesForLocales(routes: FileRoute[], config: I18nRoutingConfig): FileRoute[];
223
+ /**
224
+ * Validate a locale string (PR L2).
225
+ *
226
+ * The locale drives both URL pattern emission AND filesystem writes
227
+ * (see `expandRoutesForLocales` for full rationale). Reject input that
228
+ * would either:
229
+ * - break path-traversal boundaries (`..`, `/`, `\`)
230
+ * - produce invalid URL segments (whitespace, NUL)
231
+ * - create hidden-file artifacts (`.` leading)
232
+ * - silently kill the app (empty string)
233
+ *
234
+ * Throws with an actionable `[Pyreon]` error message. Called per-locale
235
+ * by `expandRoutesForLocales` after the empty-locales no-op guard.
236
+ *
237
+ * @internal — exported for unit testing.
238
+ */
239
+ declare function validateLocale(locale: string): void;
49
240
  /**
50
241
  * Create a LocaleContext for use in components and loaders.
51
242
  */
@@ -100,5 +291,5 @@ declare function useLocale(): string;
100
291
  */
101
292
  declare function setLocale(locale: string, config: I18nRoutingConfig): void;
102
293
  //#endregion
103
- export { I18nRoutingConfig, LocaleContext, LocaleCtx, buildLocalePath, createLocaleContext, detectLocaleFromHeader, extractLocaleFromPath, i18nRouting, localeSignal, setLocale, useLocale };
294
+ export { I18nRoutingConfig, LocaleContext, LocaleCtx, buildLocalePath, createLocaleContext, detectLocaleFromHeader, expandRoutesForLocales, extractLocaleFromPath, i18nRouting, localeSignal, setLocale, useLocale, validateLocale };
104
295
  //# sourceMappingURL=i18n-routing2.d.ts.map