@pyreon/zero 0.15.0 → 0.16.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 +275 -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 +634 -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 +575 -72
  22. package/lib/vite-plugin-xjWZwudX.js +2454 -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 +301 -10
  50. package/src/vercel-revalidate-handler.ts +204 -0
  51. package/src/vite-plugin.ts +108 -30
  52. package/lib/vite-plugin-E4BHYvYW.js +0 -855
@@ -4,6 +4,7 @@ import * as _$_pyreon_router0 from "@pyreon/router";
4
4
  import { RouteRecord } from "@pyreon/router";
5
5
  import { Middleware, MiddlewareContext } from "@pyreon/server";
6
6
  import { Plugin } from "vite";
7
+
7
8
  //#region src/app.d.ts
8
9
  interface CreateAppOptions {
9
10
  /** Route definitions (from file-based routing or manual). */
@@ -16,6 +17,18 @@ interface CreateAppOptions {
16
17
  layout?: ComponentFn;
17
18
  /** Global error component. */
18
19
  errorComponent?: ComponentFn;
20
+ /**
21
+ * Base URL prefix for the deployed app (e.g. `/blog/`). Forwarded to
22
+ * `createRouter({ base })` so RouterLinks render correctly prefixed
23
+ * hrefs (`<a href="/blog/about">` instead of `<a href="/about">`) and
24
+ * the router strips the prefix from incoming URLs before matching.
25
+ *
26
+ * Default: `'/'`. Pre-fix this was disconnected from `zero({ base })`
27
+ * — RouterLinks rendered un-prefixed hrefs even when Vite's asset URL
28
+ * rewriting was correctly using the prefix, causing client-side
29
+ * navigation to break against subpath deploys.
30
+ */
31
+ base?: string;
19
32
  }
20
33
  /**
21
34
  * Create a full Zero app — assembles router, head provider, and root layout.
@@ -61,6 +74,66 @@ interface ApiRouteEntry {
61
74
  module: ApiRouteModule;
62
75
  }
63
76
  //#endregion
77
+ //#region src/i18n-routing.d.ts
78
+ interface I18nRoutingConfig {
79
+ /** Supported locales. e.g. ["en", "de", "cs"] */
80
+ locales: string[];
81
+ /** Default locale — served without prefix (/ instead of /en/). */
82
+ defaultLocale: string;
83
+ /** Redirect root to detected locale. Default: true */
84
+ detectLocale?: boolean;
85
+ /** Cookie name to persist locale preference. Default: "locale" */
86
+ cookieName?: string;
87
+ /** URL strategy. Default: "prefix-except-default" */
88
+ strategy?: 'prefix' | 'prefix-except-default';
89
+ }
90
+ interface LocaleContext {
91
+ /** Current locale code. e.g. "en", "de" */
92
+ locale: string;
93
+ /** All supported locales. */
94
+ locales: string[];
95
+ /** Default locale. */
96
+ defaultLocale: string;
97
+ /** Build a localized path. e.g. localePath("/about", "de") → "/de/about" */
98
+ localePath: (path: string, locale?: string) => string;
99
+ /** Get hreflang alternates for the current path. */
100
+ alternates: () => Array<{
101
+ locale: string;
102
+ url: string;
103
+ }>;
104
+ }
105
+ /**
106
+ * Detect preferred locale from Accept-Language header.
107
+ */
108
+ declare function detectLocaleFromHeader(acceptLanguage: string | null | undefined, locales: string[], defaultLocale: string): string;
109
+ /**
110
+ * Create a LocaleContext for use in components and loaders.
111
+ */
112
+ declare function createLocaleContext(locale: string, path: string, config: I18nRoutingConfig): LocaleContext;
113
+ /**
114
+ * I18n routing middleware for Zero's server.
115
+ *
116
+ * - Detects locale from URL prefix or Accept-Language header
117
+ * - Redirects root to preferred locale (when detectLocale is true)
118
+ * - Sets locale context for loaders and components
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * // zero.config.ts
123
+ * import { i18nRouting } from "@pyreon/zero"
124
+ *
125
+ * export default defineConfig({
126
+ * plugins: [
127
+ * i18nRouting({
128
+ * locales: ["en", "de", "cs"],
129
+ * defaultLocale: "en",
130
+ * }),
131
+ * ],
132
+ * })
133
+ * ```
134
+ */
135
+ declare function i18nRouting(config: I18nRoutingConfig): Plugin;
136
+ //#endregion
64
137
  //#region src/types.d.ts
65
138
  type RenderMode = 'ssr' | 'ssg' | 'spa' | 'isr';
66
139
  interface ISRConfig {
@@ -73,6 +146,42 @@ interface ISRConfig {
73
146
  * space (e.g. `/user/:id` where `:id` is free-form).
74
147
  */
75
148
  maxEntries?: number;
149
+ /**
150
+ * Cache-key derivation function. The default keys cache entries by
151
+ * `url.pathname` ONLY — query strings, cookies, and headers are
152
+ * stripped.
153
+ *
154
+ * **⚠️ Auth-gated incompatibility.** The default behavior is
155
+ * unsafe for request-dependent loaders. A loader that reads
156
+ * `request.headers.get('cookie')` to gate auth will render ONCE
157
+ * with the first user's cookie, then serve that HTML to every
158
+ * subsequent user. To use ISR with personalized / auth-gated
159
+ * pages, supply a `cacheKey` that varies on the auth identifier
160
+ * (session cookie, user-id header, etc.), OR don't use ISR for
161
+ * such routes — use SSR instead.
162
+ *
163
+ * @example
164
+ * // Vary cache by session cookie:
165
+ * isr: {
166
+ * revalidate: 60,
167
+ * cacheKey: (req) => {
168
+ * const url = new URL(req.url)
169
+ * const session = req.headers.get('cookie')?.match(/session=([^;]+)/)?.[1] ?? 'anon'
170
+ * return `${url.pathname}::${session}`
171
+ * },
172
+ * }
173
+ *
174
+ * @example
175
+ * // Vary by a query parameter:
176
+ * isr: {
177
+ * revalidate: 60,
178
+ * cacheKey: (req) => {
179
+ * const url = new URL(req.url)
180
+ * return `${url.pathname}?sort=${url.searchParams.get('sort') ?? ''}`
181
+ * },
182
+ * }
183
+ */
184
+ cacheKey?: (req: Request) => string;
76
185
  }
77
186
  interface ZeroConfig {
78
187
  /** Default rendering mode. Default: "ssr" */
@@ -86,13 +195,167 @@ interface ZeroConfig {
86
195
  /** SSG options — only used when mode is "ssg". */
87
196
  ssg?: {
88
197
  /** Paths to prerender (or function returning paths). */paths?: string[] | (() => string[] | Promise<string[]>);
198
+ /**
199
+ * Auto-emit `dist/404.html` from the route tree's `_404.tsx` /
200
+ * `_not-found.tsx` convention. fs-router already wires `_404.tsx` as
201
+ * `notFoundComponent` on its parent layout route; the SSG plugin walks
202
+ * the tree, picks up the first one, renders it through the same SSR
203
+ * pipeline as regular paths (so styler CSS / @pyreon/head metadata land
204
+ * correctly), and writes the result to `dist/404.html`. Static hosts
205
+ * (Netlify, Cloudflare Pages, GitHub Pages, S3+CloudFront) serve this
206
+ * file automatically for unmatched URLs. Default: `true`. Set to
207
+ * `false` to opt out — the route tree is left alone.
208
+ */
209
+ emit404?: boolean;
210
+ /**
211
+ * When a route loader throws `redirect('/target')` during SSG, write
212
+ * a `dist/_redirects` file (Netlify / Cloudflare Pages convention)
213
+ * AND a `dist/_redirects.json` (Vercel convention) listing every
214
+ * redirected source path → target. Static hosts pick whichever
215
+ * format their platform supports automatically. The redirected
216
+ * path's HTML file is NOT emitted — the redirect is the response.
217
+ *
218
+ * Without this option, redirect-throwing loaders land in
219
+ * `errors[]` and the path silently disappears from the build —
220
+ * the user sees no output for `/old` AND no warning that the
221
+ * loader ran a redirect. Default: `true`. Set to `false` to
222
+ * restore the pre-PR-B behaviour (redirects treated as errors).
223
+ */
224
+ emitRedirects?: boolean;
225
+ /**
226
+ * Additionally emit a static HTML file at the source path with a
227
+ * `<meta http-equiv="refresh">` redirect — for adapters / hosts
228
+ * that don't read `_redirects` (plain S3, GitHub Pages, simple
229
+ * file servers). The meta-refresh fallback works on any HTTP
230
+ * server that serves static files.
231
+ *
232
+ * - `'none'` (default): only `_redirects` / `_redirects.json` are
233
+ * emitted; no per-redirect HTML file.
234
+ * - `'meta-refresh'`: emit `dist/<source>/index.html` containing
235
+ * `<meta http-equiv="refresh" content="0; url=<target>">` plus
236
+ * a canonical link tag for SEO. Status code information is
237
+ * lost (meta-refresh has no status equivalent), so 301/302/307/
238
+ * 308 all collapse to "client-side refresh".
239
+ */
240
+ redirectsAsHtml?: 'none' | 'meta-refresh';
241
+ /**
242
+ * Callback invoked when a path's render throws (loader-throw that
243
+ * isn't a `redirect()`, render exception, anything that lands in the
244
+ * `errors[]` collection). Returns either:
245
+ * - `string` → written as the path's HTML in place of the failed
246
+ * render. Use this to emit a per-path fallback page (e.g. a generic
247
+ * "this content is temporarily unavailable" template) so static
248
+ * hosts have something to serve at that URL instead of 404'ing.
249
+ * - `null` → skip; the path produces no HTML output. The error
250
+ * stays in `errors[]` for the post-build summary.
251
+ *
252
+ * The callback runs ONCE per failed path. Async callbacks are
253
+ * awaited. If the callback itself throws, the throw is captured as
254
+ * a separate error entry and the path is skipped (no fallback HTML).
255
+ * Default: `undefined` — failed paths just land in `errors[]`.
256
+ *
257
+ * @example
258
+ * ssg: {
259
+ * onPathError: async (path, error) => {
260
+ * console.error(\`SSG render failed for \${path}:\`, error)
261
+ * return \`<!DOCTYPE html><html><body><h1>Page unavailable</h1></body></html>\`
262
+ * },
263
+ * }
264
+ */
265
+ onPathError?: (path: string, error: unknown) => string | null | Promise<string | null>;
266
+ /**
267
+ * When `'json'` (default), write `dist/_pyreon-ssg-errors.json` after
268
+ * the render loop summarising every error encountered (path traversal,
269
+ * timeout, render exception, getStaticPaths throw, fallback callback
270
+ * throw). Each entry has `{ path, message, name, stack }`. The file
271
+ * is ONLY written when `errors.length > 0` — successful builds don't
272
+ * leak an empty manifest. Reading it lets CI gate on render failures
273
+ * without parsing console output (e.g.
274
+ * `cat dist/_pyreon-ssg-errors.json | jq '.errors | length' | grep -q 0`).
275
+ *
276
+ * Set to `'none'` to opt out entirely — errors stay in console-only,
277
+ * matching pre-PR-G behaviour.
278
+ */
279
+ errorArtifact?: 'json' | 'none';
280
+ /**
281
+ * Maximum number of paths rendered in parallel during the SSG closeBundle
282
+ * loop. Default: `4` — a sensible balance between speedup and the risk
283
+ * of exhausting downstream resources (DB connection pools, fetch
284
+ * rate-limits) inside loaders. Set to `1` to render fully sequentially
285
+ * (the pre-PR-D behaviour). Set to a higher value for faster builds
286
+ * on CI / multi-core hosts; the practical ceiling is the number of
287
+ * loader-side concurrent connections your app's data layer tolerates.
288
+ *
289
+ * The render-error pipeline (`onPathError` callback, `errors[]`
290
+ * collection, `_pyreon-ssg-errors.json` artifact) is unchanged —
291
+ * concurrency only affects how many paths are in flight at once,
292
+ * not how their successes / failures are recorded.
293
+ *
294
+ * @example
295
+ * ssg: {
296
+ * concurrency: 8, // Faster builds for static-content sites
297
+ * }
298
+ */
299
+ concurrency?: number;
300
+ /**
301
+ * Per-path progress callback. Invoked once per path AFTER its render
302
+ * settles (success, redirect, OR failure) — never during in-flight
303
+ * renders. Receives `{ completed, total, currentPath, elapsed }`
304
+ * where:
305
+ * - `completed` is the count of paths whose render has settled (1-indexed)
306
+ * - `total` is the full path count from `resolvePaths()`
307
+ * - `currentPath` is the path that just settled
308
+ * - `elapsed` is wall-clock ms since the loop started
309
+ *
310
+ * Use cases: build-tool progress bars (Vite picks up stdout), CI
311
+ * heartbeat lines on long builds (10k-path sites take minutes —
312
+ * silent stretches look hung), build-time perf instrumentation.
313
+ *
314
+ * Async callbacks are awaited before the next path's progress fires,
315
+ * so a slow callback can serialize progress reporting (it does NOT
316
+ * gate the worker pool — paths keep rendering in parallel; only
317
+ * the progress callbacks themselves are serialized). Throws are
318
+ * captured into `errors[]` with the path suffix `(onProgress)` so
319
+ * a buggy callback can't take down the build.
320
+ *
321
+ * @example
322
+ * ssg: {
323
+ * onProgress: ({ completed, total, currentPath, elapsed }) => {
324
+ * console.log(`[${completed}/${total}] ${currentPath} (${elapsed}ms)`)
325
+ * },
326
+ * }
327
+ */
328
+ onProgress?: (info: {
329
+ completed: number;
330
+ total: number;
331
+ currentPath: string;
332
+ elapsed: number;
333
+ }) => void | Promise<void>;
89
334
  };
90
335
  /** ISR config — only used when mode is "isr". */
91
336
  isr?: ISRConfig;
92
- /** Deploy adapter. Default: "node" */
93
- adapter?: 'node' | 'bun' | 'static' | 'vercel' | 'cloudflare' | 'netlify';
337
+ /**
338
+ * Deploy adapter. Default: `"node"`.
339
+ *
340
+ * Accepts either a built-in adapter name (string) OR a constructed
341
+ * `Adapter` instance (e.g. `vercelAdapter()`). The scaffolded templates
342
+ * emit the instance form (`adapter: vercelAdapter()`) by convention.
343
+ * `resolveAdapter` (see `adapters/index.ts`) accepts both shapes —
344
+ * strings go through a switch lookup, instances pass through
345
+ * unchanged.
346
+ */
347
+ adapter?: 'node' | 'bun' | 'static' | 'vercel' | 'cloudflare' | 'netlify' | Adapter;
94
348
  /** Base URL path. Default: "/" */
95
349
  base?: string;
350
+ /**
351
+ * i18n routing — locale-prefixed route variants generated at build time
352
+ * (PR H of the SSG roadmap). When set, every `FileRoute` is fanned into
353
+ * per-locale duplicates by `expandRoutesForLocales` from
354
+ * `@pyreon/zero`. Independent from the `i18nRouting()` Vite plugin
355
+ * (which only handles request-time locale detection); both can be used
356
+ * together. See `expandRoutesForLocales` JSDoc for strategy semantics.
357
+ */
358
+ i18n?: I18nRoutingConfig;
96
359
  /** App-level middleware applied to all routes. */
97
360
  middleware?: Middleware[];
98
361
  /** Server port for dev/preview. Default: 3000 */
@@ -135,6 +398,25 @@ interface RouteFileExports {
135
398
  * must validate session on every navigation rather than serve stale data.
136
399
  */
137
400
  hasGcTime: boolean;
401
+ /**
402
+ * Has `export function getStaticPaths` or `export const getStaticPaths`.
403
+ * Used at SSG build time to enumerate concrete values for dynamic routes
404
+ * (`/posts/[id].tsx` → `[/posts/1, /posts/2, …]`). The function returns
405
+ * `Array<{ params: Record<string, string> }>`. Mirrors Astro's per-route
406
+ * convention. Without it, dynamic routes are silently skipped during SSG
407
+ * auto-detect — the user must hand-list every value in `ssg.paths`.
408
+ */
409
+ hasGetStaticPaths: boolean;
410
+ /**
411
+ * Has `export const revalidate` (number, in seconds, or `false` for
412
+ * never-revalidate). PR I — build-time ISR. The SSG plugin emits a
413
+ * `dist/_pyreon-revalidate.json` manifest mapping `{ path: revalidate }`
414
+ * which the deploy adapter (Vercel / Cloudflare / Netlify) consumes
415
+ * to wire platform-specific ISR rebuild-on-stale. The route generator
416
+ * does NOT inline `revalidate` onto the route record — it's a
417
+ * build-time-only concern that never reaches the runtime router.
418
+ */
419
+ hasRevalidate: boolean;
138
420
  /**
139
421
  * Raw text of the `export const meta = …` initializer, captured as a
140
422
  * literal expression. When present, the route generator inlines this
@@ -154,6 +436,22 @@ interface RouteFileExports {
154
436
  * as a literal expression. Same inlining strategy as `metaLiteral`.
155
437
  */
156
438
  renderModeLiteral?: string;
439
+ /**
440
+ * Raw text of the `export const revalidate = …` initializer (e.g.
441
+ * `'60'`, `'false'`, `'3600'`). Captured at scan time so the SSG
442
+ * plugin can read the value to emit the build-time ISR manifest
443
+ * WITHOUT loading the route module — which is critical because the
444
+ * manifest is emitted from the synthetic SSR build's outer plugin
445
+ * context, where evaluating route modules would re-trigger the
446
+ * recursive sub-build env-flag guard.
447
+ *
448
+ * Only set when the revalidate export is a top-level
449
+ * `export const revalidate = <numeric|boolean literal>` that passes
450
+ * `isPureLiteral`. Anything else (function calls, references to
451
+ * other declarations) leaves this undefined and the manifest falls
452
+ * back to omitting the entry.
453
+ */
454
+ revalidateLiteral?: string;
157
455
  }
158
456
  /** Internal representation of a file-system route before conversion to RouteRecord. */
159
457
  interface FileRoute {
@@ -196,16 +494,71 @@ interface Adapter {
196
494
  name: string;
197
495
  /** Build the production server/output for this adapter. */
198
496
  build(options: AdapterBuildOptions): Promise<void>;
497
+ /**
498
+ * Revalidate a prerendered path on the deploy platform's ISR layer
499
+ * (PR I — build-time ISR). Called by user code (webhook handlers,
500
+ * cron jobs, CMS triggers, etc.) to trigger a rebuild-on-stale for
501
+ * the named path. Optional — adapters without platform ISR support
502
+ * (static, node, bun) implement a no-op. Returns `{ regenerated:
503
+ * boolean }` so user code can branch on whether the platform actually
504
+ * accepted the revalidation request.
505
+ *
506
+ * Distinct from runtime ISR (`mode: 'isr'`, on-demand LRU caching in
507
+ * `@pyreon/zero/server`'s `createISRHandler`). Build-time ISR is
508
+ * static prerender + platform-driven rebuild-on-stale; runtime ISR is
509
+ * SSR-cached-with-TTL. They can coexist.
510
+ *
511
+ * Per-route `revalidate` metadata flows from `export const revalidate
512
+ * = 60` in route files into a `dist/_pyreon-revalidate.json` manifest
513
+ * the adapter reads at deploy time. Adapters use that manifest to
514
+ * configure platform ISR (Vercel `output/config.json`, Cloudflare
515
+ * Cache API rules, Netlify revalidation headers).
516
+ */
517
+ revalidate?(path: string): Promise<AdapterRevalidateResult>;
199
518
  }
200
- interface AdapterBuildOptions {
201
- /** Path to the built server entry. */
202
- serverEntry: string;
203
- /** Path to the client build output. */
204
- clientOutDir: string;
205
- /** Final output directory. */
519
+ /**
520
+ * Result of `Adapter.revalidate(path)`. `regenerated: false` means the
521
+ * adapter does not support platform ISR (no-op fallback) OR the
522
+ * platform rejected the request. Adapters that throw on platform-API
523
+ * failure should let it propagate so user code can handle the rejection.
524
+ */
525
+ interface AdapterRevalidateResult {
526
+ regenerated: boolean;
527
+ }
528
+ /**
529
+ * Inputs the build pipeline passes to an adapter's `build()` method.
530
+ *
531
+ * The `kind` field discriminates the two shapes. **SSR mode** (`'ssr'`)
532
+ * carries `serverEntry` + `clientOutDir` so adapters can wrap the user's
533
+ * server bundle as a serverless function. **SSG mode** (`'ssg'`) carries
534
+ * only `outDir` (which IS the rendered dist/) — no serverEntry exists
535
+ * because every page is already prerendered. SSG-mode adapters write
536
+ * platform-specific routing config so the host knows the deploy is
537
+ * fully-static (no function invocation per request).
538
+ *
539
+ * Pre-PR-J this was a single SSR-shaped struct; the SSG path had no way
540
+ * to invoke `adapter.build()` because it couldn't supply `serverEntry`.
541
+ * Adding `kind` (with TS-narrowing per branch) lets `ssgPlugin`
542
+ * `closeBundle` call `adapter.build({ kind: 'ssg', outDir, config })`
543
+ * cleanly, AND keeps the SSR-mode adapter implementations unchanged.
544
+ */
545
+ type AdapterBuildOptions = {
546
+ kind: 'ssr'; /** Path to the built server entry. */
547
+ serverEntry: string; /** Path to the client build output. */
548
+ clientOutDir: string; /** Final output directory. */
206
549
  outDir: string;
207
550
  config: ZeroConfig;
208
- }
551
+ } | {
552
+ kind: 'ssg';
553
+ /**
554
+ * The rendered dist directory. For SSG, this directory IS the
555
+ * publishable output — adapters write platform-specific routing
556
+ * config alongside (e.g. `.vercel/output/config.json`,
557
+ * `_routes.json`, `netlify.toml`) but generally don't move files.
558
+ */
559
+ outDir: string;
560
+ config: ZeroConfig;
561
+ };
209
562
  //#endregion
210
563
  //#region src/entry-server.d.ts
211
564
  interface CreateServerOptions {
@@ -257,6 +610,33 @@ declare function defineConfig(config: ZeroConfig): ZeroConfig;
257
610
  declare function resolveConfig(userConfig?: ZeroConfig): Required<Pick<ZeroConfig, 'mode' | 'base' | 'port' | 'adapter'>> & ZeroConfig;
258
611
  //#endregion
259
612
  //#region src/fs-router.d.ts
613
+ /**
614
+ * Return type of a route file's `getStaticPaths()` export. Each entry
615
+ * supplies one set of concrete values for the route's dynamic segments;
616
+ * the SSG plugin expands the route's URL pattern with these params and
617
+ * renders one HTML file per entry.
618
+ *
619
+ * @example
620
+ * ```tsx
621
+ * // src/routes/posts/[id].tsx
622
+ * import type { GetStaticPaths } from '@pyreon/zero/server'
623
+ *
624
+ * export const getStaticPaths: GetStaticPaths<{ id: string }> = async () => {
625
+ * const posts = await fetch('https://api.example.com/posts').then(r => r.json())
626
+ * return posts.map((p) => ({ params: { id: p.slug } }))
627
+ * }
628
+ *
629
+ * export default function Post() { ... }
630
+ * ```
631
+ *
632
+ * For catch-all routes (`/blog/[...slug].tsx`), pass the full path through
633
+ * the catch-all param: `{ params: { slug: 'a/b' } }` → `/blog/a/b`.
634
+ */
635
+ type GetStaticPaths<TParams extends Record<string, string> = Record<string, string>> = () => Array<{
636
+ params: TParams;
637
+ }> | Promise<Array<{
638
+ params: TParams;
639
+ }>>;
260
640
  /**
261
641
  * Parse a set of file paths (relative to routes dir) into FileRoute objects.
262
642
  *
@@ -328,9 +708,97 @@ declare function scanRouteFiles(routesDir: string): Promise<string[]>;
328
708
  */
329
709
  declare function createISRHandler(handler: (req: Request) => Promise<Response>, config: ISRConfig): (req: Request) => Promise<Response>;
330
710
  //#endregion
711
+ //#region src/vercel-revalidate-handler.d.ts
712
+ /**
713
+ * M3.1 — Drop-in Vercel revalidate webhook handler.
714
+ *
715
+ * Pre-M3.1 the `vercelAdapter.revalidate(path)` (PR I) POSTed to
716
+ * `/api/_pyreon-revalidate?path=...&secret=...` — a CONVENTION that users
717
+ * had to implement themselves. This helper scaffolds the convention:
718
+ *
719
+ * // src/routes/api/_pyreon-revalidate.ts (or `pages/api/...` in
720
+ * // Next-style apps deployed to Vercel)
721
+ * export { vercelRevalidateHandler as default } from '@pyreon/zero/server'
722
+ *
723
+ * The handler validates the secret query param against
724
+ * `VERCEL_REVALIDATE_TOKEN`, validates the path is in the build-time
725
+ * revalidate manifest, and calls Vercel's `res.revalidate(path)` API.
726
+ *
727
+ * Returns a standard `(req: Request) => Response` Web API handler — works
728
+ * with Vercel Edge functions, Node serverless functions (via Vercel's
729
+ * `@vercel/node` adapter that bridges Node `req`/`res` to Web standard
730
+ * fetch shapes), and the in-process `mode: 'ssr'` runtime.
731
+ *
732
+ * @example
733
+ * // src/routes/api/_pyreon-revalidate.ts
734
+ * import { vercelRevalidateHandler } from '@pyreon/zero/server'
735
+ *
736
+ * export const POST = vercelRevalidateHandler({
737
+ * // Optional — defaults to reading `_pyreon-revalidate.json` from cwd.
738
+ * manifestPath: './dist/_pyreon-revalidate.json',
739
+ * })
740
+ *
741
+ * @example
742
+ * // Custom revalidate impl (e.g. for a self-hosted Pyreon SSR runtime
743
+ * // that wants build-time revalidate behavior without Vercel's
744
+ * // `res.revalidate()` API):
745
+ * export const POST = vercelRevalidateHandler({
746
+ * onRevalidate: async (path) => {
747
+ * // Clear your in-process ISR cache, emit a metrics event, etc.
748
+ * await myCache.invalidate(path)
749
+ * },
750
+ * })
751
+ */
752
+ interface VercelRevalidateHandlerOptions {
753
+ /**
754
+ * Absolute or cwd-relative path to the `_pyreon-revalidate.json` manifest.
755
+ * Defaults to `./dist/_pyreon-revalidate.json` (the standard SSG output).
756
+ *
757
+ * The handler refuses to revalidate paths NOT in this manifest — protects
758
+ * against arbitrary-path revalidation attacks even when the secret leaks.
759
+ */
760
+ manifestPath?: string;
761
+ /**
762
+ * Custom revalidation impl. Defaults to calling Vercel's `res.revalidate()`
763
+ * API via the dynamic `@vercel/node`-bridged response object on globalThis
764
+ * (Vercel injects it for serverless functions).
765
+ *
766
+ * Supply this when running OUTSIDE Vercel (self-hosted SSR with a custom
767
+ * in-process ISR cache, edge runtimes that have their own purge API, etc.).
768
+ * Receives the validated path; throw to signal failure (handler returns 500).
769
+ */
770
+ onRevalidate?: (path: string) => void | Promise<void>;
771
+ /**
772
+ * Override the env-var name the handler reads the secret from. Default
773
+ * `VERCEL_REVALIDATE_TOKEN` matches the adapter's `revalidate()` write.
774
+ * Useful when adopting the helper outside Vercel and the production
775
+ * webhook uses a different secret name.
776
+ */
777
+ secretEnvVar?: string;
778
+ }
779
+ /**
780
+ * Create the Web-standard request handler. Reads the manifest once on first
781
+ * invocation (cached in-process) so repeated revalidations don't re-read the
782
+ * file. Manifest read failures cache the failure too — until next process
783
+ * restart, all requests get the same 500 response (signals deploy-time misconfig).
784
+ */
785
+ declare function vercelRevalidateHandler(options?: VercelRevalidateHandlerOptions): (req: Request) => Promise<Response>;
786
+ /**
787
+ * Reset the in-process manifest cache. Test-only — production code never
788
+ * reaches this. Used by unit tests to exercise the "manifest changed
789
+ * between requests" path without spinning up a new handler.
790
+ * @internal
791
+ */
792
+ declare function _resetVercelRevalidateHandlerCache(handler: (req: Request) => Promise<Response>): void;
793
+ //#endregion
331
794
  //#region src/adapters/bun.d.ts
332
795
  /**
333
796
  * Bun adapter — generates a standalone Bun.serve() entry.
797
+ *
798
+ * **SSG mode (PR J)**: no-op. Bun adapter exists for serving the SSR
799
+ * runtime; SSG output is already complete static HTML — serve it with
800
+ * any static-file server (`bun preview` / `bunx serve` / nginx / Caddy).
801
+ * Use `staticAdapter()` if you want explicit SSG semantics.
334
802
  */
335
803
  declare function bunAdapter(): Adapter;
336
804
  //#endregion
@@ -384,6 +852,11 @@ declare function netlifyAdapter(): Adapter;
384
852
  //#region src/adapters/node.d.ts
385
853
  /**
386
854
  * Node.js adapter — generates a standalone server entry using node:http.
855
+ *
856
+ * **SSG mode (PR J)**: no-op. Node adapter exists for serving the SSR
857
+ * runtime; SSG output is already complete static HTML — serve it with
858
+ * any static-file server (`bun preview` / nginx / Caddy / `npx serve`).
859
+ * Use `staticAdapter()` if you want explicit SSG semantics.
387
860
  */
388
861
  declare function nodeAdapter(): Adapter;
389
862
  //#endregion
@@ -391,6 +864,15 @@ declare function nodeAdapter(): Adapter;
391
864
  /**
392
865
  * Static adapter — just copies the client build output.
393
866
  * Used with SSG mode where all pages are pre-rendered at build time.
867
+ *
868
+ * **SSG mode (PR J)**: no-op — `outDir` already IS the dist directory
869
+ * the SSG plugin produced. Copying it onto itself would only fail. The
870
+ * static adapter is the canonical zero-overhead deploy target for
871
+ * pure-static sites.
872
+ *
873
+ * **SSR mode**: copies clientOutDir → outDir. Calling `static` with SSR
874
+ * mode is unusual — the static adapter doesn't support server-side
875
+ * execution — but preserved as a "client-only output packager".
394
876
  */
395
877
  declare function staticAdapter(): Adapter;
396
878
  //#endregion
@@ -419,6 +901,16 @@ declare function vercelAdapter(): Adapter;
419
901
  /**
420
902
  * Resolve the adapter from config.
421
903
  * Returns a built-in adapter or throws if unknown.
904
+ *
905
+ * Accepts BOTH forms — the `ZeroConfig.adapter` type advertises string
906
+ * names (`'vercel'` / `'cloudflare'` / …) but the scaffolded templates
907
+ * historically emit `adapter: vercelAdapter()` (an Adapter instance via
908
+ * the named factory). Both work: a string goes through the switch lookup;
909
+ * an Adapter object (duck-typed via `name` + `build` fields) passes
910
+ * through. Pre-PR-J `resolveAdapter` was never called from production
911
+ * code so the string-vs-object mismatch was invisible; PR J wires the
912
+ * call into `ssgPlugin.closeBundle`, surfacing the contract divergence.
913
+ * The passthrough preserves both shapes without a breaking type change.
422
914
  */
423
915
  declare function resolveAdapter(config: ZeroConfig): Adapter;
424
916
  //#endregion
@@ -465,6 +957,12 @@ declare function compose(...middlewares: Middleware[]): Middleware;
465
957
  declare function getContext(ctx: MiddlewareContext): Record<string, unknown>;
466
958
  //#endregion
467
959
  //#region src/vite-plugin.d.ts
960
+ /**
961
+ * Retrieve the `ZeroConfig` that was passed to `zeroPlugin(userConfig)`
962
+ * when the plugin was created. Returns `undefined` if the argument
963
+ * isn't a recognized pyreon-zero main plugin instance.
964
+ */
965
+ declare function getZeroPluginConfig(plugin: Plugin): ZeroConfig | undefined;
468
966
  /**
469
967
  * Zero Vite plugin — adds file-based routing and zero-config conventions
470
968
  * on top of @pyreon/vite-plugin.
@@ -588,6 +1086,62 @@ interface SitemapConfig {
588
1086
  priority?: number;
589
1087
  /** Additional URLs to include (for dynamic routes). */
590
1088
  additionalPaths?: SitemapEntry[];
1089
+ /**
1090
+ * When `true` AND the build is running in SSG mode, the sitemap reads
1091
+ * the resolved-paths manifest emitted by the SSG plugin
1092
+ * (`dist/_pyreon-ssg-paths.json`) and includes EVERY prerendered URL —
1093
+ * including dynamic routes enumerated via `getStaticPaths` (PR A) and
1094
+ * per-locale variants (PR H, when shipped). Without this flag the
1095
+ * sitemap walks the file-system route tree directly and silently
1096
+ * skips dynamic routes (`[id]` / `[...slug]`) because their concrete
1097
+ * values aren't knowable without running each route's enumerator.
1098
+ *
1099
+ * Sequencing: when `true`, sitemap.xml emission moves from Vite's
1100
+ * `generateBundle` hook (where the SSG plugin's path enumeration
1101
+ * hasn't run yet) to `closeBundle` with `enforce: 'post'` so it
1102
+ * runs AFTER the SSG plugin. The user must ensure `seoPlugin()` is
1103
+ * placed AFTER `zero()` in the Vite plugin array (the canonical
1104
+ * ordering — `closeBundle` hooks fire in plugin-registration order).
1105
+ *
1106
+ * Falls back gracefully: when the manifest doesn't exist (mode is
1107
+ * not `ssg`, or the SSG step was skipped), the sitemap still walks
1108
+ * the file-system routes — same shape as without this flag.
1109
+ *
1110
+ * Default: `false` (preserves prior behaviour). Set `true` for SSG
1111
+ * sites that ship dynamic-route enumerations.
1112
+ */
1113
+ useSsgPaths?: boolean;
1114
+ /**
1115
+ * Emit `<xhtml:link rel="alternate" hreflang="...">` cross-references
1116
+ * inside each `<url>` entry, declaring the locale variants of every
1117
+ * page (PR K — i18n follow-up).
1118
+ *
1119
+ * Accepts:
1120
+ * - `true` — read the i18n config from the SSG paths manifest
1121
+ * (which `zero({ i18n: ... })` automatically embeds when SSG runs).
1122
+ * Zero-config win — declare i18n once, sitemap picks it up.
1123
+ * - `I18nRoutingConfig` — pass the i18n config explicitly. Use when
1124
+ * the project doesn't run SSG (file-scan sitemap path) but still
1125
+ * wants hreflang in the emitted sitemap.
1126
+ * - `false` / omitted — no hreflang, plain `<url>` entries.
1127
+ *
1128
+ * The emitted shape per page-cluster is the Google-recommended form:
1129
+ *
1130
+ * <url>
1131
+ * <loc>https://example.com/about</loc>
1132
+ * <xhtml:link rel="alternate" hreflang="en" href="https://example.com/about"/>
1133
+ * <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/about"/>
1134
+ * <xhtml:link rel="alternate" hreflang="cs" href="https://example.com/cs/about"/>
1135
+ * <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/about"/>
1136
+ * </url>
1137
+ *
1138
+ * The `x-default` entry points at the default-locale URL so search
1139
+ * engines have a fallback when the user's language doesn't match any
1140
+ * of the configured locales. URLs are clustered by their un-prefixed
1141
+ * (default-locale) form — `/about`, `/de/about`, `/cs/about` collapse
1142
+ * into ONE `<url>` entry with three `xhtml:link` siblings.
1143
+ */
1144
+ hreflang?: boolean | I18nRoutingConfig;
591
1145
  }
592
1146
  interface SitemapEntry {
593
1147
  path: string;
@@ -598,8 +1152,14 @@ interface SitemapEntry {
598
1152
  type ChangeFreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
599
1153
  /**
600
1154
  * Generate a sitemap.xml string from route file paths.
1155
+ *
1156
+ * When `i18n` is set (PR K — passed by `seoPlugin` after reading the
1157
+ * i18n config from `zero({ i18n: ... })`), URLs are clustered by their
1158
+ * un-prefixed (default-locale) form and each `<url>` carries
1159
+ * `<xhtml:link rel="alternate" hreflang="...">` siblings for every
1160
+ * locale variant + an `x-default` entry pointing at the default locale.
601
1161
  */
602
- declare function generateSitemap(routeFiles: string[], config: SitemapConfig): string;
1162
+ declare function generateSitemap(routeFiles: string[], config: SitemapConfig, i18n?: I18nRoutingConfig): string;
603
1163
  interface RobotsConfig {
604
1164
  /** Rules per user-agent. */
605
1165
  rules?: RobotsRule[];
@@ -649,7 +1209,10 @@ interface SeoPluginConfig {
649
1209
  * pyreon(),
650
1210
  * zero(),
651
1211
  * seoPlugin({
652
- * sitemap: { origin: "https://example.com" },
1212
+ * sitemap: {
1213
+ * origin: "https://example.com",
1214
+ * useSsgPaths: true, // include dynamic-route enumerations
1215
+ * },
653
1216
  * robots: { sitemap: "https://example.com/sitemap.xml" },
654
1217
  * }),
655
1218
  * ],
@@ -904,65 +1467,5 @@ declare function inferJsonLd(options: InferJsonLdOptions): Record<string, unknow
904
1467
  */
905
1468
  declare function aiPlugin(config: AiPluginConfig): Plugin;
906
1469
  //#endregion
907
- //#region src/i18n-routing.d.ts
908
- interface I18nRoutingConfig {
909
- /** Supported locales. e.g. ["en", "de", "cs"] */
910
- locales: string[];
911
- /** Default locale — served without prefix (/ instead of /en/). */
912
- defaultLocale: string;
913
- /** Redirect root to detected locale. Default: true */
914
- detectLocale?: boolean;
915
- /** Cookie name to persist locale preference. Default: "locale" */
916
- cookieName?: string;
917
- /** URL strategy. Default: "prefix-except-default" */
918
- strategy?: 'prefix' | 'prefix-except-default';
919
- }
920
- interface LocaleContext {
921
- /** Current locale code. e.g. "en", "de" */
922
- locale: string;
923
- /** All supported locales. */
924
- locales: string[];
925
- /** Default locale. */
926
- defaultLocale: string;
927
- /** Build a localized path. e.g. localePath("/about", "de") → "/de/about" */
928
- localePath: (path: string, locale?: string) => string;
929
- /** Get hreflang alternates for the current path. */
930
- alternates: () => Array<{
931
- locale: string;
932
- url: string;
933
- }>;
934
- }
935
- /**
936
- * Detect preferred locale from Accept-Language header.
937
- */
938
- declare function detectLocaleFromHeader(acceptLanguage: string | null | undefined, locales: string[], defaultLocale: string): string;
939
- /**
940
- * Create a LocaleContext for use in components and loaders.
941
- */
942
- declare function createLocaleContext(locale: string, path: string, config: I18nRoutingConfig): LocaleContext;
943
- /**
944
- * I18n routing middleware for Zero's server.
945
- *
946
- * - Detects locale from URL prefix or Accept-Language header
947
- * - Redirects root to preferred locale (when detectLocale is true)
948
- * - Sets locale context for loaders and components
949
- *
950
- * @example
951
- * ```ts
952
- * // zero.config.ts
953
- * import { i18nRouting } from "@pyreon/zero"
954
- *
955
- * export default defineConfig({
956
- * plugins: [
957
- * i18nRouting({
958
- * locales: ["en", "de", "cs"],
959
- * defaultLocale: "en",
960
- * }),
961
- * ],
962
- * })
963
- * ```
964
- */
965
- declare function i18nRouting(config: I18nRoutingConfig): Plugin;
966
- //#endregion
967
- export { type AiPluginConfig, type CreateAppOptions, type CreateServerOptions, type FaviconLocaleConfig, type FaviconPluginConfig, type GenerateRouteModuleOptions, type InferJsonLdOptions, type OgImageLayer, type OgImagePluginConfig, type OgImageTemplate, type RobotsConfig, type SeoPluginConfig, type SitemapConfig, aiPlugin, bunAdapter, cloudflareAdapter, compose, createApp, createISRHandler, createLocaleContext, createServer, zeroPlugin as default, defineConfig, detectLocaleFromHeader, faviconLinks, faviconPlugin, filePathToUrlPath, generateLlmsFullTxt, generateLlmsTxt, generateMiddlewareModule, generateRobots, generateRouteModule, generateSitemap, getContext, i18nRouting, inferJsonLd, jsonLd, netlifyAdapter, nodeAdapter, ogImagePath, ogImagePlugin, parseFileRoutes, render404Page, resolveAdapter, resolveConfig, scanRouteFiles, seoMiddleware, seoPlugin, staticAdapter, vercelAdapter };
1470
+ export { type AiPluginConfig, type CreateAppOptions, type CreateServerOptions, type FaviconLocaleConfig, type FaviconPluginConfig, type GenerateRouteModuleOptions, type GetStaticPaths, type InferJsonLdOptions, type OgImageLayer, type OgImagePluginConfig, type OgImageTemplate, type RobotsConfig, type SeoPluginConfig, type SitemapConfig, type VercelRevalidateHandlerOptions, _resetVercelRevalidateHandlerCache, aiPlugin, bunAdapter, cloudflareAdapter, compose, createApp, createISRHandler, createLocaleContext, createServer, zeroPlugin as default, defineConfig, detectLocaleFromHeader, faviconLinks, faviconPlugin, filePathToUrlPath, generateLlmsFullTxt, generateLlmsTxt, generateMiddlewareModule, generateRobots, generateRouteModule, generateSitemap, getContext, getZeroPluginConfig, i18nRouting, inferJsonLd, jsonLd, netlifyAdapter, nodeAdapter, ogImagePath, ogImagePlugin, parseFileRoutes, render404Page, resolveAdapter, resolveConfig, scanRouteFiles, seoMiddleware, seoPlugin, staticAdapter, vercelAdapter, vercelRevalidateHandler };
968
1471
  //# sourceMappingURL=server2.d.ts.map