@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
@@ -1,5 +1,5 @@
1
1
  import * as _$_pyreon_core0 from "@pyreon/core";
2
- import { ComponentFn, VNodeChild } from "@pyreon/core";
2
+ import { ComponentFn, Ref, VNodeChild } from "@pyreon/core";
3
3
  import * as _$_pyreon_reactivity0 from "@pyreon/reactivity";
4
4
  import { LoaderContext, NavigationGuard } from "@pyreon/router";
5
5
  import { Middleware } from "@pyreon/server";
@@ -47,6 +47,9 @@ interface ImageProps {
47
47
  * Raw mode — renders a plain `<img>` without the container div,
48
48
  * aspect-ratio, max-width, or lazy loading wrapper.
49
49
  * Use when the Image is inside a custom layout (absolute positioning, etc.).
50
+ *
51
+ * Note: `raw` skips the three-layer API entirely. `useImage` / `createImage`
52
+ * do not apply when `raw: true` — the component returns a bare `<img>`.
50
53
  */
51
54
  raw?: boolean;
52
55
  }
@@ -54,9 +57,106 @@ interface ImageSource {
54
57
  src: string;
55
58
  width: number;
56
59
  }
60
+ /** Return type of {@link useImage}. */
61
+ interface UseImageReturn {
62
+ /** Ref — attach to the container element for IntersectionObserver. */
63
+ containerRef: Ref<HTMLElement>;
64
+ /** Whether the image has entered the viewport (and started loading). */
65
+ inView: () => boolean;
66
+ /** Whether the `<img>` onLoad has fired. */
67
+ loaded: () => boolean;
68
+ /** Resolved `src` accessor — empty string until inView, then `props.src`. */
69
+ src: () => string;
70
+ /** Resolved srcSet accessor — empty until inView; empty when `formats` is set (srcset moves to `<source>` elements). */
71
+ srcSet: () => string;
72
+ /** `sizes` attribute or undefined when no srcset. */
73
+ sizes: string | undefined;
74
+ /** `aspect-ratio` CSS value (`"${width} / ${height}"`). */
75
+ aspectRatio: string;
76
+ /** Resolved CSS for the container — position + overflow + aspect-ratio + max-width + caller's `style`. */
77
+ containerStyle: string;
78
+ /** Resolved CSS accessor for the `<img>` — fit + transition + opacity (placeholder fade). */
79
+ imageStyle: () => string;
80
+ /** Resolved CSS accessor for the placeholder `<img>` (only meaningful when `placeholder` is set). */
81
+ placeholderStyle: () => string;
82
+ /** `loading` attribute — eager when priority/eager, else lazy. */
83
+ loading: 'lazy' | 'eager';
84
+ /** `fetchPriority` — 'high' when priority, else undefined. */
85
+ fetchPriority: 'high' | undefined;
86
+ /** onLoad handler — sets the loaded signal. Wire into the rendered `<img>`. */
87
+ handleLoad: () => void;
88
+ /** Resolved per-format <source> descriptors (or undefined when no formats). */
89
+ formats: FormatSource[] | undefined;
90
+ /** Whether `formats` is non-empty (i.e. consumer should render a `<picture>` wrapper). */
91
+ hasFormats: boolean;
92
+ }
93
+ /** Props passed to a custom component via {@link createImage}. */
94
+ interface ImageRenderProps {
95
+ /** Container ref. */
96
+ containerRef: Ref<HTMLElement>;
97
+ /** CSS class for the container. */
98
+ class: string | undefined;
99
+ /** Resolved container `style` string. */
100
+ containerStyle: string;
101
+ /** Pre-rendered placeholder `<img>` (or `null` when `placeholder` is unset). */
102
+ placeholder: VNodeChild;
103
+ /** Pre-rendered image — either a bare `<img>` or a `<picture>` tree when `formats` is set. */
104
+ image: VNodeChild;
105
+ }
57
106
  /**
58
- * Optimized image component with lazy loading, responsive images,
59
- * multi-format <picture> support, and blur-up placeholders.
107
+ * Composable that provides all image optimization behavior lazy loading,
108
+ * srcset/sizes resolution, format selection, blur-placeholder state,
109
+ * load tracking.
110
+ *
111
+ * Use this for full control when `createImage` is too opinionated about
112
+ * the surrounding markup (e.g. custom container layouts, non-`<div>`
113
+ * wrappers, additional overlay elements).
114
+ *
115
+ * @example
116
+ * function MyImage(props: ImageProps) {
117
+ * const img = useImage(props)
118
+ * return (
119
+ * <figure ref={img.containerRef} style={img.containerStyle}>
120
+ * <img
121
+ * src={img.src}
122
+ * srcSet={img.srcSet}
123
+ * sizes={img.sizes}
124
+ * alt={props.alt}
125
+ * loading={img.loading}
126
+ * onLoad={img.handleLoad}
127
+ * style={img.imageStyle}
128
+ * />
129
+ * <figcaption>{props.alt}</figcaption>
130
+ * </figure>
131
+ * )
132
+ * }
133
+ */
134
+ declare function useImage(props: ImageProps): UseImageReturn;
135
+ /**
136
+ * Higher-order component that wraps any component with image optimization.
137
+ *
138
+ * The wrapped component receives {@link ImageRenderProps} with the pre-rendered
139
+ * `image` JSX (bare `<img>` OR `<picture>` tree depending on formats), the
140
+ * pre-rendered `placeholder` JSX, and the container ref + styles. Consumers
141
+ * compose those pieces with whatever wrapper element / layout they want.
142
+ *
143
+ * @example
144
+ * // Custom figure-based image with caption
145
+ * const FigureImage = createImage((props) => (
146
+ * <figure ref={props.containerRef} class={props.class} style={props.containerStyle}>
147
+ * {props.placeholder}
148
+ * {props.image}
149
+ * <figcaption>Caption goes here</figcaption>
150
+ * </figure>
151
+ * ))
152
+ *
153
+ * // Usage — identical to default <Image>
154
+ * <FigureImage src="/hero.jpg" alt="Hero" width={1200} height={630} />
155
+ */
156
+ declare function createImage(Component: (p: ImageRenderProps) => any): (props: ImageProps) => any;
157
+ /**
158
+ * Default optimized image component with lazy loading, responsive srcset,
159
+ * `<picture>` multi-format support, and blur-up placeholders.
60
160
  *
61
161
  * @example
62
162
  * // With imagePlugin — spread the import directly
@@ -67,7 +167,7 @@ interface ImageSource {
67
167
  * // Manual usage
68
168
  * <Image src="/hero.jpg" alt="Hero" width={1200} height={630} />
69
169
  */
70
- declare function Image(props: ImageProps): VNodeChild;
170
+ declare const Image: (props: ImageProps) => any;
71
171
  //#endregion
72
172
  //#region src/link.d.ts
73
173
  interface LinkProps {
@@ -209,14 +309,86 @@ interface ScriptProps {
209
309
  id?: string;
210
310
  /** Async attribute. Default: true */
211
311
  async?: boolean;
212
- /** onLoad callback. */
312
+ /** onLoad callback — fires when the `<script>` finishes loading. */
213
313
  onLoad?: () => void;
214
- /** onError callback. */
314
+ /** onError callback — fires when the `<script>` fails to load. */
215
315
  onError?: (error: Error) => void;
216
316
  }
217
317
  type ScriptStrategy = 'beforeHydration' | 'afterHydration' | 'onIdle' | 'onInteraction' | 'onViewport';
318
+ /** Return type of {@link useScript}. */
319
+ interface UseScriptReturn {
320
+ /** Ref — attach to the sentinel element for `onViewport` strategy. Undefined for other strategies. */
321
+ sentinelRef: Ref<HTMLElement> | undefined;
322
+ /** Whether the script has finished loading (onLoad fired). */
323
+ loaded: () => boolean;
324
+ /** Whether the script load failed (onError fired). */
325
+ errored: () => boolean;
326
+ /** Whether the script is in the strategy state machine awaiting a trigger (idle/interaction/viewport). */
327
+ pending: () => boolean;
328
+ /** Whether the consumer needs to render a sentinel element (only true for `onViewport`). */
329
+ needsSentinel: boolean;
330
+ /** Imperatively trigger the script load. Already invoked automatically by the strategy. */
331
+ load: () => void;
332
+ }
333
+ /** Props passed to a custom component via {@link createScript}. */
334
+ interface ScriptRenderProps {
335
+ /** Ref — attach to whatever sentinel element you render (only matters for `onViewport`). */
336
+ sentinelRef: Ref<HTMLElement> | undefined;
337
+ /** Whether the script is in viewport-wait mode (true → render a sentinel; false → render null). */
338
+ needsSentinel: boolean;
339
+ /** Whether the script has finished loading (onLoad fired). */
340
+ loaded: () => boolean;
341
+ /** Whether the script load failed (onError fired). */
342
+ errored: () => boolean;
343
+ /** Whether the script is in the strategy state machine awaiting a trigger. */
344
+ pending: () => boolean;
345
+ }
346
+ /**
347
+ * Composable that provides all script loading behavior — strategy state
348
+ * machine (afterHydration / onIdle / onInteraction / onViewport),
349
+ * deduplication, load/error tracking.
350
+ *
351
+ * Returns reactive signals (`loaded`, `errored`, `pending`) so consumers
352
+ * can render loading indicators, retry buttons, or analytics-readiness
353
+ * gates without re-implementing the strategy machine.
354
+ *
355
+ * @example
356
+ * function MyScript(props: ScriptProps) {
357
+ * const s = useScript(props)
358
+ * return (
359
+ * <>
360
+ * {() => s.loaded() ? <Analytics /> : <Skeleton />}
361
+ * {() => s.needsSentinel && <div ref={s.sentinelRef} style="width:0;height:0" />}
362
+ * </>
363
+ * )
364
+ * }
365
+ */
366
+ declare function useScript(props: ScriptProps): UseScriptReturn;
218
367
  /**
219
- * Optimized script loading component.
368
+ * Higher-order component that wraps any component with script load behavior.
369
+ *
370
+ * The wrapped component receives {@link ScriptRenderProps} with the sentinel
371
+ * ref, load-state signals, and a `needsSentinel` flag. Use this when you want
372
+ * to render a loading indicator, retry button, or custom analytics-readiness
373
+ * gate around the script load.
374
+ *
375
+ * @example
376
+ * // Script with a loading indicator
377
+ * const TrackedScript = createScript((props) => (
378
+ * <>
379
+ * {() => props.pending() && <Spinner />}
380
+ * {() => props.errored() && <button onClick={() => location.reload()}>Retry</button>}
381
+ * {props.needsSentinel && <div ref={props.sentinelRef} style="width:0;height:0" />}
382
+ * </>
383
+ * ))
384
+ *
385
+ * <TrackedScript src="/analytics.js" strategy="onIdle" />
386
+ */
387
+ declare function createScript(Component: (p: ScriptRenderProps) => any): (props: ScriptProps) => any;
388
+ /**
389
+ * Default optimized script component. Renders a 0×0 sentinel `<div>` for the
390
+ * `onViewport` strategy (so IntersectionObserver has an element to observe),
391
+ * `null` for every other strategy.
220
392
  *
221
393
  * @example
222
394
  * // Load analytics after page is interactive
@@ -230,7 +402,460 @@ type ScriptStrategy = 'beforeHydration' | 'afterHydration' | 'onIdle' | 'onInter
230
402
  * {`console.log("App hydrated!")`}
231
403
  * </Script>
232
404
  */
233
- declare function Script(props: ScriptProps): VNodeChild;
405
+ declare const Script: (props: ScriptProps) => VNodeChild;
406
+ //#endregion
407
+ //#region src/types.d.ts
408
+ /** What a route file (e.g. `src/routes/index.tsx`) can export. */
409
+ interface RouteModule {
410
+ /** Default export is the page component. */
411
+ default?: ComponentFn;
412
+ /** Layout wrapper — wraps this route and all children. */
413
+ layout?: ComponentFn;
414
+ /** Loading component shown while lazy-loading or during Suspense. */
415
+ loading?: ComponentFn;
416
+ /** Error component shown when the route errors. */
417
+ error?: ComponentFn;
418
+ /** Server-side data loader. */
419
+ loader?: (ctx: LoaderContext) => Promise<unknown>;
420
+ /** Per-route middleware. */
421
+ middleware?: Middleware | Middleware[];
422
+ /** Navigation guard — can redirect or block navigation. */
423
+ guard?: NavigationGuard;
424
+ /** Route metadata. */
425
+ meta?: RouteMeta;
426
+ /** Rendering mode override for this route. */
427
+ renderMode?: RenderMode;
428
+ }
429
+ /** Per-route metadata. */
430
+ interface RouteMeta {
431
+ title?: string;
432
+ description?: string;
433
+ [key: string]: unknown;
434
+ }
435
+ type RenderMode = 'ssr' | 'ssg' | 'spa' | 'isr';
436
+ interface ISRConfig {
437
+ /** Revalidation interval in seconds. */
438
+ revalidate: number;
439
+ /**
440
+ * Maximum number of distinct URL paths to keep in the in-memory cache.
441
+ * Oldest-first LRU eviction once the cap is reached. Default: `1000`.
442
+ * Set higher for SSG-heavy sites, lower for routes with unbounded URL
443
+ * space (e.g. `/user/:id` where `:id` is free-form).
444
+ */
445
+ maxEntries?: number;
446
+ /**
447
+ * Cache-key derivation function. The default keys cache entries by
448
+ * `url.pathname` ONLY — query strings, cookies, and headers are
449
+ * stripped.
450
+ *
451
+ * **⚠️ Auth-gated incompatibility.** The default behavior is
452
+ * unsafe for request-dependent loaders. A loader that reads
453
+ * `request.headers.get('cookie')` to gate auth will render ONCE
454
+ * with the first user's cookie, then serve that HTML to every
455
+ * subsequent user. To use ISR with personalized / auth-gated
456
+ * pages, supply a `cacheKey` that varies on the auth identifier
457
+ * (session cookie, user-id header, etc.), OR don't use ISR for
458
+ * such routes — use SSR instead.
459
+ *
460
+ * @example
461
+ * // Vary cache by session cookie:
462
+ * isr: {
463
+ * revalidate: 60,
464
+ * cacheKey: (req) => {
465
+ * const url = new URL(req.url)
466
+ * const session = req.headers.get('cookie')?.match(/session=([^;]+)/)?.[1] ?? 'anon'
467
+ * return `${url.pathname}::${session}`
468
+ * },
469
+ * }
470
+ *
471
+ * @example
472
+ * // Vary by a query parameter:
473
+ * isr: {
474
+ * revalidate: 60,
475
+ * cacheKey: (req) => {
476
+ * const url = new URL(req.url)
477
+ * return `${url.pathname}?sort=${url.searchParams.get('sort') ?? ''}`
478
+ * },
479
+ * }
480
+ */
481
+ cacheKey?: (req: Request) => string;
482
+ }
483
+ interface ZeroConfig {
484
+ /** Default rendering mode. Default: "ssr" */
485
+ mode?: RenderMode;
486
+ /** Vite config overrides. */
487
+ vite?: Record<string, unknown>;
488
+ /** SSR options. */
489
+ ssr?: {
490
+ /** Streaming mode. Default: "string" */mode?: 'string' | 'stream';
491
+ };
492
+ /** SSG options — only used when mode is "ssg". */
493
+ ssg?: {
494
+ /** Paths to prerender (or function returning paths). */paths?: string[] | (() => string[] | Promise<string[]>);
495
+ /**
496
+ * Auto-emit `dist/404.html` from the route tree's `_404.tsx` /
497
+ * `_not-found.tsx` convention. fs-router already wires `_404.tsx` as
498
+ * `notFoundComponent` on its parent layout route; the SSG plugin walks
499
+ * the tree, picks up the first one, renders it through the same SSR
500
+ * pipeline as regular paths (so styler CSS / @pyreon/head metadata land
501
+ * correctly), and writes the result to `dist/404.html`. Static hosts
502
+ * (Netlify, Cloudflare Pages, GitHub Pages, S3+CloudFront) serve this
503
+ * file automatically for unmatched URLs. Default: `true`. Set to
504
+ * `false` to opt out — the route tree is left alone.
505
+ */
506
+ emit404?: boolean;
507
+ /**
508
+ * When a route loader throws `redirect('/target')` during SSG, write
509
+ * a `dist/_redirects` file (Netlify / Cloudflare Pages convention)
510
+ * AND a `dist/_redirects.json` (Vercel convention) listing every
511
+ * redirected source path → target. Static hosts pick whichever
512
+ * format their platform supports automatically. The redirected
513
+ * path's HTML file is NOT emitted — the redirect is the response.
514
+ *
515
+ * Without this option, redirect-throwing loaders land in
516
+ * `errors[]` and the path silently disappears from the build —
517
+ * the user sees no output for `/old` AND no warning that the
518
+ * loader ran a redirect. Default: `true`. Set to `false` to
519
+ * restore the pre-PR-B behaviour (redirects treated as errors).
520
+ */
521
+ emitRedirects?: boolean;
522
+ /**
523
+ * Additionally emit a static HTML file at the source path with a
524
+ * `<meta http-equiv="refresh">` redirect — for adapters / hosts
525
+ * that don't read `_redirects` (plain S3, GitHub Pages, simple
526
+ * file servers). The meta-refresh fallback works on any HTTP
527
+ * server that serves static files.
528
+ *
529
+ * - `'none'` (default): only `_redirects` / `_redirects.json` are
530
+ * emitted; no per-redirect HTML file.
531
+ * - `'meta-refresh'`: emit `dist/<source>/index.html` containing
532
+ * `<meta http-equiv="refresh" content="0; url=<target>">` plus
533
+ * a canonical link tag for SEO. Status code information is
534
+ * lost (meta-refresh has no status equivalent), so 301/302/307/
535
+ * 308 all collapse to "client-side refresh".
536
+ */
537
+ redirectsAsHtml?: 'none' | 'meta-refresh';
538
+ /**
539
+ * Callback invoked when a path's render throws (loader-throw that
540
+ * isn't a `redirect()`, render exception, anything that lands in the
541
+ * `errors[]` collection). Returns either:
542
+ * - `string` → written as the path's HTML in place of the failed
543
+ * render. Use this to emit a per-path fallback page (e.g. a generic
544
+ * "this content is temporarily unavailable" template) so static
545
+ * hosts have something to serve at that URL instead of 404'ing.
546
+ * - `null` → skip; the path produces no HTML output. The error
547
+ * stays in `errors[]` for the post-build summary.
548
+ *
549
+ * The callback runs ONCE per failed path. Async callbacks are
550
+ * awaited. If the callback itself throws, the throw is captured as
551
+ * a separate error entry and the path is skipped (no fallback HTML).
552
+ * Default: `undefined` — failed paths just land in `errors[]`.
553
+ *
554
+ * @example
555
+ * ssg: {
556
+ * onPathError: async (path, error) => {
557
+ * console.error(\`SSG render failed for \${path}:\`, error)
558
+ * return \`<!DOCTYPE html><html><body><h1>Page unavailable</h1></body></html>\`
559
+ * },
560
+ * }
561
+ */
562
+ onPathError?: (path: string, error: unknown) => string | null | Promise<string | null>;
563
+ /**
564
+ * When `'json'` (default), write `dist/_pyreon-ssg-errors.json` after
565
+ * the render loop summarising every error encountered (path traversal,
566
+ * timeout, render exception, getStaticPaths throw, fallback callback
567
+ * throw). Each entry has `{ path, message, name, stack }`. The file
568
+ * is ONLY written when `errors.length > 0` — successful builds don't
569
+ * leak an empty manifest. Reading it lets CI gate on render failures
570
+ * without parsing console output (e.g.
571
+ * `cat dist/_pyreon-ssg-errors.json | jq '.errors | length' | grep -q 0`).
572
+ *
573
+ * Set to `'none'` to opt out entirely — errors stay in console-only,
574
+ * matching pre-PR-G behaviour.
575
+ */
576
+ errorArtifact?: 'json' | 'none';
577
+ /**
578
+ * Maximum number of paths rendered in parallel during the SSG closeBundle
579
+ * loop. Default: `4` — a sensible balance between speedup and the risk
580
+ * of exhausting downstream resources (DB connection pools, fetch
581
+ * rate-limits) inside loaders. Set to `1` to render fully sequentially
582
+ * (the pre-PR-D behaviour). Set to a higher value for faster builds
583
+ * on CI / multi-core hosts; the practical ceiling is the number of
584
+ * loader-side concurrent connections your app's data layer tolerates.
585
+ *
586
+ * The render-error pipeline (`onPathError` callback, `errors[]`
587
+ * collection, `_pyreon-ssg-errors.json` artifact) is unchanged —
588
+ * concurrency only affects how many paths are in flight at once,
589
+ * not how their successes / failures are recorded.
590
+ *
591
+ * @example
592
+ * ssg: {
593
+ * concurrency: 8, // Faster builds for static-content sites
594
+ * }
595
+ */
596
+ concurrency?: number;
597
+ /**
598
+ * Per-path progress callback. Invoked once per path AFTER its render
599
+ * settles (success, redirect, OR failure) — never during in-flight
600
+ * renders. Receives `{ completed, total, currentPath, elapsed }`
601
+ * where:
602
+ * - `completed` is the count of paths whose render has settled (1-indexed)
603
+ * - `total` is the full path count from `resolvePaths()`
604
+ * - `currentPath` is the path that just settled
605
+ * - `elapsed` is wall-clock ms since the loop started
606
+ *
607
+ * Use cases: build-tool progress bars (Vite picks up stdout), CI
608
+ * heartbeat lines on long builds (10k-path sites take minutes —
609
+ * silent stretches look hung), build-time perf instrumentation.
610
+ *
611
+ * Async callbacks are awaited before the next path's progress fires,
612
+ * so a slow callback can serialize progress reporting (it does NOT
613
+ * gate the worker pool — paths keep rendering in parallel; only
614
+ * the progress callbacks themselves are serialized). Throws are
615
+ * captured into `errors[]` with the path suffix `(onProgress)` so
616
+ * a buggy callback can't take down the build.
617
+ *
618
+ * @example
619
+ * ssg: {
620
+ * onProgress: ({ completed, total, currentPath, elapsed }) => {
621
+ * console.log(`[${completed}/${total}] ${currentPath} (${elapsed}ms)`)
622
+ * },
623
+ * }
624
+ */
625
+ onProgress?: (info: {
626
+ completed: number;
627
+ total: number;
628
+ currentPath: string;
629
+ elapsed: number;
630
+ }) => void | Promise<void>;
631
+ };
632
+ /** ISR config — only used when mode is "isr". */
633
+ isr?: ISRConfig;
634
+ /**
635
+ * Deploy adapter. Default: `"node"`.
636
+ *
637
+ * Accepts either a built-in adapter name (string) OR a constructed
638
+ * `Adapter` instance (e.g. `vercelAdapter()`). The scaffolded templates
639
+ * emit the instance form (`adapter: vercelAdapter()`) by convention.
640
+ * `resolveAdapter` (see `adapters/index.ts`) accepts both shapes —
641
+ * strings go through a switch lookup, instances pass through
642
+ * unchanged.
643
+ */
644
+ adapter?: 'node' | 'bun' | 'static' | 'vercel' | 'cloudflare' | 'netlify' | Adapter;
645
+ /** Base URL path. Default: "/" */
646
+ base?: string;
647
+ /**
648
+ * i18n routing — locale-prefixed route variants generated at build time
649
+ * (PR H of the SSG roadmap). When set, every `FileRoute` is fanned into
650
+ * per-locale duplicates by `expandRoutesForLocales` from
651
+ * `@pyreon/zero`. Independent from the `i18nRouting()` Vite plugin
652
+ * (which only handles request-time locale detection); both can be used
653
+ * together. See `expandRoutesForLocales` JSDoc for strategy semantics.
654
+ */
655
+ i18n?: I18nRoutingConfig;
656
+ /** App-level middleware applied to all routes. */
657
+ middleware?: Middleware[];
658
+ /** Server port for dev/preview. Default: 3000 */
659
+ port?: number;
660
+ }
661
+ /**
662
+ * Which optional metadata exports a route file declares.
663
+ * Detected at scan time by parsing the file source. The code generator
664
+ * uses this to skip emitting `import * as mod` for routes that only
665
+ * export `default`, eliminating the dual-import collision with `lazy()`
666
+ * and silencing `IMPORT_IS_UNDEFINED` warnings from Rolldown.
667
+ */
668
+ interface RouteFileExports {
669
+ /** Has `export const loader` or `export function loader` */
670
+ hasLoader: boolean;
671
+ /** Has `export const guard` or `export function guard` */
672
+ hasGuard: boolean;
673
+ /** Has `export const meta` */
674
+ hasMeta: boolean;
675
+ /** Has `export const renderMode` */
676
+ hasRenderMode: boolean;
677
+ /** Has `export const error` (custom per-route error component) */
678
+ hasError: boolean;
679
+ /** Has `export const middleware` */
680
+ hasMiddleware: boolean;
681
+ /**
682
+ * Has `export const loaderKey` or `export function loaderKey`. When present,
683
+ * the route generator wires it as the `loaderKey` field on the route record,
684
+ * which controls cache identity for `_loaderCache`. Useful for auth-gate
685
+ * loaders that should invalidate when the session cookie changes — read
686
+ * `document.cookie` (CSR) or `ctx.request.headers.get('cookie')` (SSR) and
687
+ * derive a key from session identity. Default cache key is `path + params`,
688
+ * which doesn't see cookie changes.
689
+ */
690
+ hasLoaderKey: boolean;
691
+ /**
692
+ * Has `export const gcTime` (number, in ms). When present, the route generator
693
+ * inlines it on the route record. `gcTime: 0` disables caching entirely —
694
+ * the loader runs on every navigation. Useful for auth-gate loaders that
695
+ * must validate session on every navigation rather than serve stale data.
696
+ */
697
+ hasGcTime: boolean;
698
+ /**
699
+ * Has `export function getStaticPaths` or `export const getStaticPaths`.
700
+ * Used at SSG build time to enumerate concrete values for dynamic routes
701
+ * (`/posts/[id].tsx` → `[/posts/1, /posts/2, …]`). The function returns
702
+ * `Array<{ params: Record<string, string> }>`. Mirrors Astro's per-route
703
+ * convention. Without it, dynamic routes are silently skipped during SSG
704
+ * auto-detect — the user must hand-list every value in `ssg.paths`.
705
+ */
706
+ hasGetStaticPaths: boolean;
707
+ /**
708
+ * Has `export const revalidate` (number, in seconds, or `false` for
709
+ * never-revalidate). PR I — build-time ISR. The SSG plugin emits a
710
+ * `dist/_pyreon-revalidate.json` manifest mapping `{ path: revalidate }`
711
+ * which the deploy adapter (Vercel / Cloudflare / Netlify) consumes
712
+ * to wire platform-specific ISR rebuild-on-stale. The route generator
713
+ * does NOT inline `revalidate` onto the route record — it's a
714
+ * build-time-only concern that never reaches the runtime router.
715
+ */
716
+ hasRevalidate: boolean;
717
+ /**
718
+ * Raw text of the `export const meta = …` initializer, captured as a
719
+ * literal expression. When present, the route generator inlines this
720
+ * value directly into the generated routes module instead of importing
721
+ * it from the route file — which means the route file can be lazy()'d
722
+ * without forcing the entire dependency tree into the main bundle.
723
+ *
724
+ * Only set when the meta export is a top-level `export const meta = { … }`
725
+ * literal that can be extracted via balanced-brace scanning. Anything
726
+ * fancier (computed values, function calls, references to other
727
+ * declarations) leaves this undefined and falls back to a static module
728
+ * import.
729
+ */
730
+ metaLiteral?: string;
731
+ /**
732
+ * Raw text of the `export const renderMode = …` initializer, captured
733
+ * as a literal expression. Same inlining strategy as `metaLiteral`.
734
+ */
735
+ renderModeLiteral?: string;
736
+ /**
737
+ * Raw text of the `export const revalidate = …` initializer (e.g.
738
+ * `'60'`, `'false'`, `'3600'`). Captured at scan time so the SSG
739
+ * plugin can read the value to emit the build-time ISR manifest
740
+ * WITHOUT loading the route module — which is critical because the
741
+ * manifest is emitted from the synthetic SSR build's outer plugin
742
+ * context, where evaluating route modules would re-trigger the
743
+ * recursive sub-build env-flag guard.
744
+ *
745
+ * Only set when the revalidate export is a top-level
746
+ * `export const revalidate = <numeric|boolean literal>` that passes
747
+ * `isPureLiteral`. Anything else (function calls, references to
748
+ * other declarations) leaves this undefined and the manifest falls
749
+ * back to omitting the entry.
750
+ */
751
+ revalidateLiteral?: string;
752
+ }
753
+ /** Internal representation of a file-system route before conversion to RouteRecord. */
754
+ interface FileRoute {
755
+ /** File path relative to routes dir (e.g. "users/[id].tsx") */
756
+ filePath: string;
757
+ /** Parsed URL path pattern (e.g. "/users/:id") */
758
+ urlPath: string;
759
+ /** Directory path for grouping (e.g. "users" or "" for root) */
760
+ dirPath: string;
761
+ /** Route segment depth for nesting. */
762
+ depth: number;
763
+ /** Whether this is a layout file. */
764
+ isLayout: boolean;
765
+ /** Whether this is an error boundary file. */
766
+ isError: boolean;
767
+ /** Whether this is a loading fallback file. */
768
+ isLoading: boolean;
769
+ /** Whether this is a not-found (404) file. */
770
+ isNotFound: boolean;
771
+ /** Whether this is a catch-all route. */
772
+ isCatchAll: boolean;
773
+ /** Resolved rendering mode. */
774
+ renderMode: RenderMode;
775
+ /**
776
+ * Detected optional exports from the file source.
777
+ * When undefined, the generator treats the file as having no metadata
778
+ * exports and emits the optimal `lazy()` shape (one dynamic import,
779
+ * no static metadata wiring). When provided, the generator emits a
780
+ * single namespace import for files with metadata or `lazy()` for
781
+ * files with only a default export.
782
+ */
783
+ exports?: RouteFileExports;
784
+ }
785
+ /** Entry mapping a URL pattern to its route-level middleware. */
786
+ interface RouteMiddlewareEntry {
787
+ pattern: string;
788
+ middleware: Middleware | Middleware[];
789
+ }
790
+ interface Adapter {
791
+ name: string;
792
+ /** Build the production server/output for this adapter. */
793
+ build(options: AdapterBuildOptions): Promise<void>;
794
+ /**
795
+ * Revalidate a prerendered path on the deploy platform's ISR layer
796
+ * (PR I — build-time ISR). Called by user code (webhook handlers,
797
+ * cron jobs, CMS triggers, etc.) to trigger a rebuild-on-stale for
798
+ * the named path. Optional — adapters without platform ISR support
799
+ * (static, node, bun) implement a no-op. Returns `{ regenerated:
800
+ * boolean }` so user code can branch on whether the platform actually
801
+ * accepted the revalidation request.
802
+ *
803
+ * Distinct from runtime ISR (`mode: 'isr'`, on-demand LRU caching in
804
+ * `@pyreon/zero/server`'s `createISRHandler`). Build-time ISR is
805
+ * static prerender + platform-driven rebuild-on-stale; runtime ISR is
806
+ * SSR-cached-with-TTL. They can coexist.
807
+ *
808
+ * Per-route `revalidate` metadata flows from `export const revalidate
809
+ * = 60` in route files into a `dist/_pyreon-revalidate.json` manifest
810
+ * the adapter reads at deploy time. Adapters use that manifest to
811
+ * configure platform ISR (Vercel `output/config.json`, Cloudflare
812
+ * Cache API rules, Netlify revalidation headers).
813
+ */
814
+ revalidate?(path: string): Promise<AdapterRevalidateResult>;
815
+ }
816
+ /**
817
+ * Result of `Adapter.revalidate(path)`. `regenerated: false` means the
818
+ * adapter does not support platform ISR (no-op fallback) OR the
819
+ * platform rejected the request. Adapters that throw on platform-API
820
+ * failure should let it propagate so user code can handle the rejection.
821
+ */
822
+ interface AdapterRevalidateResult {
823
+ regenerated: boolean;
824
+ }
825
+ /**
826
+ * Inputs the build pipeline passes to an adapter's `build()` method.
827
+ *
828
+ * The `kind` field discriminates the two shapes. **SSR mode** (`'ssr'`)
829
+ * carries `serverEntry` + `clientOutDir` so adapters can wrap the user's
830
+ * server bundle as a serverless function. **SSG mode** (`'ssg'`) carries
831
+ * only `outDir` (which IS the rendered dist/) — no serverEntry exists
832
+ * because every page is already prerendered. SSG-mode adapters write
833
+ * platform-specific routing config so the host knows the deploy is
834
+ * fully-static (no function invocation per request).
835
+ *
836
+ * Pre-PR-J this was a single SSR-shaped struct; the SSG path had no way
837
+ * to invoke `adapter.build()` because it couldn't supply `serverEntry`.
838
+ * Adding `kind` (with TS-narrowing per branch) lets `ssgPlugin`
839
+ * `closeBundle` call `adapter.build({ kind: 'ssg', outDir, config })`
840
+ * cleanly, AND keeps the SSR-mode adapter implementations unchanged.
841
+ */
842
+ type AdapterBuildOptions = {
843
+ kind: 'ssr'; /** Path to the built server entry. */
844
+ serverEntry: string; /** Path to the client build output. */
845
+ clientOutDir: string; /** Final output directory. */
846
+ outDir: string;
847
+ config: ZeroConfig;
848
+ } | {
849
+ kind: 'ssg';
850
+ /**
851
+ * The rendered dist directory. For SSG, this directory IS the
852
+ * publishable output — adapters write platform-specific routing
853
+ * config alongside (e.g. `.vercel/output/config.json`,
854
+ * `_routes.json`, `netlify.toml`) but generally don't move files.
855
+ */
856
+ outDir: string;
857
+ config: ZeroConfig;
858
+ };
234
859
  //#endregion
235
860
  //#region src/i18n-routing.d.ts
236
861
  interface I18nRoutingConfig {
@@ -494,179 +1119,6 @@ declare function ThemeToggle(props: {
494
1119
  */
495
1120
  declare const themeScript = "(function(){try{var t=localStorage.getItem(\"zero-theme\");var r=t===\"light\"?\"light\":t===\"dark\"?\"dark\":window.matchMedia(\"(prefers-color-scheme:dark)\").matches?\"dark\":\"light\";document.documentElement.dataset.theme=r;document.querySelectorAll(\"[data-favicon-theme]\").forEach(function(l){l.media=l.dataset.faviconTheme===r?\"\":\"not all\"})}catch(e){}})()";
496
1121
  //#endregion
497
- //#region src/types.d.ts
498
- /** What a route file (e.g. `src/routes/index.tsx`) can export. */
499
- interface RouteModule {
500
- /** Default export is the page component. */
501
- default?: ComponentFn;
502
- /** Layout wrapper — wraps this route and all children. */
503
- layout?: ComponentFn;
504
- /** Loading component shown while lazy-loading or during Suspense. */
505
- loading?: ComponentFn;
506
- /** Error component shown when the route errors. */
507
- error?: ComponentFn;
508
- /** Server-side data loader. */
509
- loader?: (ctx: LoaderContext) => Promise<unknown>;
510
- /** Per-route middleware. */
511
- middleware?: Middleware | Middleware[];
512
- /** Navigation guard — can redirect or block navigation. */
513
- guard?: NavigationGuard;
514
- /** Route metadata. */
515
- meta?: RouteMeta;
516
- /** Rendering mode override for this route. */
517
- renderMode?: RenderMode;
518
- }
519
- /** Per-route metadata. */
520
- interface RouteMeta {
521
- title?: string;
522
- description?: string;
523
- [key: string]: unknown;
524
- }
525
- type RenderMode = 'ssr' | 'ssg' | 'spa' | 'isr';
526
- interface ISRConfig {
527
- /** Revalidation interval in seconds. */
528
- revalidate: number;
529
- /**
530
- * Maximum number of distinct URL paths to keep in the in-memory cache.
531
- * Oldest-first LRU eviction once the cap is reached. Default: `1000`.
532
- * Set higher for SSG-heavy sites, lower for routes with unbounded URL
533
- * space (e.g. `/user/:id` where `:id` is free-form).
534
- */
535
- maxEntries?: number;
536
- }
537
- interface ZeroConfig {
538
- /** Default rendering mode. Default: "ssr" */
539
- mode?: RenderMode;
540
- /** Vite config overrides. */
541
- vite?: Record<string, unknown>;
542
- /** SSR options. */
543
- ssr?: {
544
- /** Streaming mode. Default: "string" */mode?: 'string' | 'stream';
545
- };
546
- /** SSG options — only used when mode is "ssg". */
547
- ssg?: {
548
- /** Paths to prerender (or function returning paths). */paths?: string[] | (() => string[] | Promise<string[]>);
549
- };
550
- /** ISR config — only used when mode is "isr". */
551
- isr?: ISRConfig;
552
- /** Deploy adapter. Default: "node" */
553
- adapter?: 'node' | 'bun' | 'static' | 'vercel' | 'cloudflare' | 'netlify';
554
- /** Base URL path. Default: "/" */
555
- base?: string;
556
- /** App-level middleware applied to all routes. */
557
- middleware?: Middleware[];
558
- /** Server port for dev/preview. Default: 3000 */
559
- port?: number;
560
- }
561
- /**
562
- * Which optional metadata exports a route file declares.
563
- * Detected at scan time by parsing the file source. The code generator
564
- * uses this to skip emitting `import * as mod` for routes that only
565
- * export `default`, eliminating the dual-import collision with `lazy()`
566
- * and silencing `IMPORT_IS_UNDEFINED` warnings from Rolldown.
567
- */
568
- interface RouteFileExports {
569
- /** Has `export const loader` or `export function loader` */
570
- hasLoader: boolean;
571
- /** Has `export const guard` or `export function guard` */
572
- hasGuard: boolean;
573
- /** Has `export const meta` */
574
- hasMeta: boolean;
575
- /** Has `export const renderMode` */
576
- hasRenderMode: boolean;
577
- /** Has `export const error` (custom per-route error component) */
578
- hasError: boolean;
579
- /** Has `export const middleware` */
580
- hasMiddleware: boolean;
581
- /**
582
- * Has `export const loaderKey` or `export function loaderKey`. When present,
583
- * the route generator wires it as the `loaderKey` field on the route record,
584
- * which controls cache identity for `_loaderCache`. Useful for auth-gate
585
- * loaders that should invalidate when the session cookie changes — read
586
- * `document.cookie` (CSR) or `ctx.request.headers.get('cookie')` (SSR) and
587
- * derive a key from session identity. Default cache key is `path + params`,
588
- * which doesn't see cookie changes.
589
- */
590
- hasLoaderKey: boolean;
591
- /**
592
- * Has `export const gcTime` (number, in ms). When present, the route generator
593
- * inlines it on the route record. `gcTime: 0` disables caching entirely —
594
- * the loader runs on every navigation. Useful for auth-gate loaders that
595
- * must validate session on every navigation rather than serve stale data.
596
- */
597
- hasGcTime: boolean;
598
- /**
599
- * Raw text of the `export const meta = …` initializer, captured as a
600
- * literal expression. When present, the route generator inlines this
601
- * value directly into the generated routes module instead of importing
602
- * it from the route file — which means the route file can be lazy()'d
603
- * without forcing the entire dependency tree into the main bundle.
604
- *
605
- * Only set when the meta export is a top-level `export const meta = { … }`
606
- * literal that can be extracted via balanced-brace scanning. Anything
607
- * fancier (computed values, function calls, references to other
608
- * declarations) leaves this undefined and falls back to a static module
609
- * import.
610
- */
611
- metaLiteral?: string;
612
- /**
613
- * Raw text of the `export const renderMode = …` initializer, captured
614
- * as a literal expression. Same inlining strategy as `metaLiteral`.
615
- */
616
- renderModeLiteral?: string;
617
- }
618
- /** Internal representation of a file-system route before conversion to RouteRecord. */
619
- interface FileRoute {
620
- /** File path relative to routes dir (e.g. "users/[id].tsx") */
621
- filePath: string;
622
- /** Parsed URL path pattern (e.g. "/users/:id") */
623
- urlPath: string;
624
- /** Directory path for grouping (e.g. "users" or "" for root) */
625
- dirPath: string;
626
- /** Route segment depth for nesting. */
627
- depth: number;
628
- /** Whether this is a layout file. */
629
- isLayout: boolean;
630
- /** Whether this is an error boundary file. */
631
- isError: boolean;
632
- /** Whether this is a loading fallback file. */
633
- isLoading: boolean;
634
- /** Whether this is a not-found (404) file. */
635
- isNotFound: boolean;
636
- /** Whether this is a catch-all route. */
637
- isCatchAll: boolean;
638
- /** Resolved rendering mode. */
639
- renderMode: RenderMode;
640
- /**
641
- * Detected optional exports from the file source.
642
- * When undefined, the generator treats the file as having no metadata
643
- * exports and emits the optimal `lazy()` shape (one dynamic import,
644
- * no static metadata wiring). When provided, the generator emits a
645
- * single namespace import for files with metadata or `lazy()` for
646
- * files with only a default export.
647
- */
648
- exports?: RouteFileExports;
649
- }
650
- /** Entry mapping a URL pattern to its route-level middleware. */
651
- interface RouteMiddlewareEntry {
652
- pattern: string;
653
- middleware: Middleware | Middleware[];
654
- }
655
- interface Adapter {
656
- name: string;
657
- /** Build the production server/output for this adapter. */
658
- build(options: AdapterBuildOptions): Promise<void>;
659
- }
660
- interface AdapterBuildOptions {
661
- /** Path to the built server entry. */
662
- serverEntry: string;
663
- /** Path to the client build output. */
664
- clientOutDir: string;
665
- /** Final output directory. */
666
- outDir: string;
667
- config: ZeroConfig;
668
- }
669
- //#endregion
670
1122
  //#region src/index.d.ts
671
1123
  /** @deprecated Import from `@pyreon/zero/favicon` instead */
672
1124
  declare function faviconPlugin(..._: unknown[]): never;
@@ -683,5 +1135,5 @@ declare function ogImagePlugin(..._: unknown[]): never;
683
1135
  /** @deprecated Import from `@pyreon/zero/ai` instead */
684
1136
  declare function aiPlugin(..._: unknown[]): never;
685
1137
  //#endregion
686
- export { type Adapter, type AdapterBuildOptions, type FileRoute, type I18nRoutingConfig, type ISRConfig, Image, type ImageProps, type ImageSource, Link, type LinkProps, type LinkRenderProps, type LoaderContext, type LocaleContext, Meta, type MetaProps, type RenderMode, type RouteMeta, type RouteMiddlewareEntry, type RouteModule, Script, type ScriptProps, type ScriptStrategy, type Theme, ThemeToggle, type UseLinkReturn, type ZeroConfig, aiPlugin, buildLocalePath, buildMetaTags, createLink, createServer, defineConfig, extractLocaleFromPath, faviconPlugin, initTheme, ogImagePlugin, prefetchRoute, resolvedTheme, seoPlugin, setLocale, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme, useLink, useLocale, validateEnv };
1138
+ export { type Adapter, type AdapterBuildOptions, type FileRoute, type I18nRoutingConfig, type ISRConfig, Image, type ImageProps, type ImageRenderProps, type ImageSource, Link, type LinkProps, type LinkRenderProps, type LoaderContext, type LocaleContext, Meta, type MetaProps, type RenderMode, type RouteMeta, type RouteMiddlewareEntry, type RouteModule, Script, type ScriptProps, type ScriptRenderProps, type ScriptStrategy, type Theme, ThemeToggle, type UseImageReturn, type UseLinkReturn, type UseScriptReturn, type ZeroConfig, aiPlugin, buildLocalePath, buildMetaTags, createImage, createLink, createScript, createServer, defineConfig, extractLocaleFromPath, faviconPlugin, initTheme, ogImagePlugin, prefetchRoute, resolvedTheme, seoPlugin, setLocale, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme, useImage, useLink, useLocale, useScript, validateEnv };
687
1139
  //# sourceMappingURL=index2.d.ts.map