@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,4 +1,4 @@
1
- import { VNodeChild } from "@pyreon/core";
1
+ import { Ref, VNodeChild } from "@pyreon/core";
2
2
 
3
3
  //#region src/script.d.ts
4
4
  interface ScriptProps {
@@ -12,14 +12,86 @@ interface ScriptProps {
12
12
  id?: string;
13
13
  /** Async attribute. Default: true */
14
14
  async?: boolean;
15
- /** onLoad callback. */
15
+ /** onLoad callback — fires when the `<script>` finishes loading. */
16
16
  onLoad?: () => void;
17
- /** onError callback. */
17
+ /** onError callback — fires when the `<script>` fails to load. */
18
18
  onError?: (error: Error) => void;
19
19
  }
20
20
  type ScriptStrategy = 'beforeHydration' | 'afterHydration' | 'onIdle' | 'onInteraction' | 'onViewport';
21
+ /** Return type of {@link useScript}. */
22
+ interface UseScriptReturn {
23
+ /** Ref — attach to the sentinel element for `onViewport` strategy. Undefined for other strategies. */
24
+ sentinelRef: Ref<HTMLElement> | undefined;
25
+ /** Whether the script has finished loading (onLoad fired). */
26
+ loaded: () => boolean;
27
+ /** Whether the script load failed (onError fired). */
28
+ errored: () => boolean;
29
+ /** Whether the script is in the strategy state machine awaiting a trigger (idle/interaction/viewport). */
30
+ pending: () => boolean;
31
+ /** Whether the consumer needs to render a sentinel element (only true for `onViewport`). */
32
+ needsSentinel: boolean;
33
+ /** Imperatively trigger the script load. Already invoked automatically by the strategy. */
34
+ load: () => void;
35
+ }
36
+ /** Props passed to a custom component via {@link createScript}. */
37
+ interface ScriptRenderProps {
38
+ /** Ref — attach to whatever sentinel element you render (only matters for `onViewport`). */
39
+ sentinelRef: Ref<HTMLElement> | undefined;
40
+ /** Whether the script is in viewport-wait mode (true → render a sentinel; false → render null). */
41
+ needsSentinel: boolean;
42
+ /** Whether the script has finished loading (onLoad fired). */
43
+ loaded: () => boolean;
44
+ /** Whether the script load failed (onError fired). */
45
+ errored: () => boolean;
46
+ /** Whether the script is in the strategy state machine awaiting a trigger. */
47
+ pending: () => boolean;
48
+ }
49
+ /**
50
+ * Composable that provides all script loading behavior — strategy state
51
+ * machine (afterHydration / onIdle / onInteraction / onViewport),
52
+ * deduplication, load/error tracking.
53
+ *
54
+ * Returns reactive signals (`loaded`, `errored`, `pending`) so consumers
55
+ * can render loading indicators, retry buttons, or analytics-readiness
56
+ * gates without re-implementing the strategy machine.
57
+ *
58
+ * @example
59
+ * function MyScript(props: ScriptProps) {
60
+ * const s = useScript(props)
61
+ * return (
62
+ * <>
63
+ * {() => s.loaded() ? <Analytics /> : <Skeleton />}
64
+ * {() => s.needsSentinel && <div ref={s.sentinelRef} style="width:0;height:0" />}
65
+ * </>
66
+ * )
67
+ * }
68
+ */
69
+ declare function useScript(props: ScriptProps): UseScriptReturn;
70
+ /**
71
+ * Higher-order component that wraps any component with script load behavior.
72
+ *
73
+ * The wrapped component receives {@link ScriptRenderProps} with the sentinel
74
+ * ref, load-state signals, and a `needsSentinel` flag. Use this when you want
75
+ * to render a loading indicator, retry button, or custom analytics-readiness
76
+ * gate around the script load.
77
+ *
78
+ * @example
79
+ * // Script with a loading indicator
80
+ * const TrackedScript = createScript((props) => (
81
+ * <>
82
+ * {() => props.pending() && <Spinner />}
83
+ * {() => props.errored() && <button onClick={() => location.reload()}>Retry</button>}
84
+ * {props.needsSentinel && <div ref={props.sentinelRef} style="width:0;height:0" />}
85
+ * </>
86
+ * ))
87
+ *
88
+ * <TrackedScript src="/analytics.js" strategy="onIdle" />
89
+ */
90
+ declare function createScript(Component: (p: ScriptRenderProps) => any): (props: ScriptProps) => any;
21
91
  /**
22
- * Optimized script loading component.
92
+ * Default optimized script component. Renders a 0×0 sentinel `<div>` for the
93
+ * `onViewport` strategy (so IntersectionObserver has an element to observe),
94
+ * `null` for every other strategy.
23
95
  *
24
96
  * @example
25
97
  * // Load analytics after page is interactive
@@ -33,7 +105,7 @@ type ScriptStrategy = 'beforeHydration' | 'afterHydration' | 'onIdle' | 'onInter
33
105
  * {`console.log("App hydrated!")`}
34
106
  * </Script>
35
107
  */
36
- declare function Script(props: ScriptProps): VNodeChild;
108
+ declare const Script: (props: ScriptProps) => VNodeChild;
37
109
  //#endregion
38
- export { Script, ScriptProps, ScriptStrategy };
110
+ export { Script, ScriptProps, ScriptRenderProps, ScriptStrategy, UseScriptReturn, createScript, useScript };
39
111
  //# sourceMappingURL=script2.d.ts.map
@@ -1,6 +1,19 @@
1
1
  import { Middleware } from "@pyreon/server";
2
2
  import { Plugin } from "vite";
3
-
3
+ //#region src/i18n-routing.d.ts
4
+ interface I18nRoutingConfig {
5
+ /** Supported locales. e.g. ["en", "de", "cs"] */
6
+ locales: string[];
7
+ /** Default locale — served without prefix (/ instead of /en/). */
8
+ defaultLocale: string;
9
+ /** Redirect root to detected locale. Default: true */
10
+ detectLocale?: boolean;
11
+ /** Cookie name to persist locale preference. Default: "locale" */
12
+ cookieName?: string;
13
+ /** URL strategy. Default: "prefix-except-default" */
14
+ strategy?: 'prefix' | 'prefix-except-default';
15
+ }
16
+ //#endregion
4
17
  //#region src/seo.d.ts
5
18
  interface SitemapConfig {
6
19
  /** Base URL of the site (required). e.g. "https://example.com" */
@@ -13,6 +26,62 @@ interface SitemapConfig {
13
26
  priority?: number;
14
27
  /** Additional URLs to include (for dynamic routes). */
15
28
  additionalPaths?: SitemapEntry[];
29
+ /**
30
+ * When `true` AND the build is running in SSG mode, the sitemap reads
31
+ * the resolved-paths manifest emitted by the SSG plugin
32
+ * (`dist/_pyreon-ssg-paths.json`) and includes EVERY prerendered URL —
33
+ * including dynamic routes enumerated via `getStaticPaths` (PR A) and
34
+ * per-locale variants (PR H, when shipped). Without this flag the
35
+ * sitemap walks the file-system route tree directly and silently
36
+ * skips dynamic routes (`[id]` / `[...slug]`) because their concrete
37
+ * values aren't knowable without running each route's enumerator.
38
+ *
39
+ * Sequencing: when `true`, sitemap.xml emission moves from Vite's
40
+ * `generateBundle` hook (where the SSG plugin's path enumeration
41
+ * hasn't run yet) to `closeBundle` with `enforce: 'post'` so it
42
+ * runs AFTER the SSG plugin. The user must ensure `seoPlugin()` is
43
+ * placed AFTER `zero()` in the Vite plugin array (the canonical
44
+ * ordering — `closeBundle` hooks fire in plugin-registration order).
45
+ *
46
+ * Falls back gracefully: when the manifest doesn't exist (mode is
47
+ * not `ssg`, or the SSG step was skipped), the sitemap still walks
48
+ * the file-system routes — same shape as without this flag.
49
+ *
50
+ * Default: `false` (preserves prior behaviour). Set `true` for SSG
51
+ * sites that ship dynamic-route enumerations.
52
+ */
53
+ useSsgPaths?: boolean;
54
+ /**
55
+ * Emit `<xhtml:link rel="alternate" hreflang="...">` cross-references
56
+ * inside each `<url>` entry, declaring the locale variants of every
57
+ * page (PR K — i18n follow-up).
58
+ *
59
+ * Accepts:
60
+ * - `true` — read the i18n config from the SSG paths manifest
61
+ * (which `zero({ i18n: ... })` automatically embeds when SSG runs).
62
+ * Zero-config win — declare i18n once, sitemap picks it up.
63
+ * - `I18nRoutingConfig` — pass the i18n config explicitly. Use when
64
+ * the project doesn't run SSG (file-scan sitemap path) but still
65
+ * wants hreflang in the emitted sitemap.
66
+ * - `false` / omitted — no hreflang, plain `<url>` entries.
67
+ *
68
+ * The emitted shape per page-cluster is the Google-recommended form:
69
+ *
70
+ * <url>
71
+ * <loc>https://example.com/about</loc>
72
+ * <xhtml:link rel="alternate" hreflang="en" href="https://example.com/about"/>
73
+ * <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/about"/>
74
+ * <xhtml:link rel="alternate" hreflang="cs" href="https://example.com/cs/about"/>
75
+ * <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/about"/>
76
+ * </url>
77
+ *
78
+ * The `x-default` entry points at the default-locale URL so search
79
+ * engines have a fallback when the user's language doesn't match any
80
+ * of the configured locales. URLs are clustered by their un-prefixed
81
+ * (default-locale) form — `/about`, `/de/about`, `/cs/about` collapse
82
+ * into ONE `<url>` entry with three `xhtml:link` siblings.
83
+ */
84
+ hreflang?: boolean | I18nRoutingConfig;
16
85
  }
17
86
  interface SitemapEntry {
18
87
  path: string;
@@ -23,8 +92,60 @@ interface SitemapEntry {
23
92
  type ChangeFreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
24
93
  /**
25
94
  * Generate a sitemap.xml string from route file paths.
95
+ *
96
+ * When `i18n` is set (PR K — passed by `seoPlugin` after reading the
97
+ * i18n config from `zero({ i18n: ... })`), URLs are clustered by their
98
+ * un-prefixed (default-locale) form and each `<url>` carries
99
+ * `<xhtml:link rel="alternate" hreflang="...">` siblings for every
100
+ * locale variant + an `x-default` entry pointing at the default locale.
101
+ */
102
+ declare function generateSitemap(routeFiles: string[], config: SitemapConfig, i18n?: I18nRoutingConfig): string;
103
+ /**
104
+ * Cluster URL entries by their un-prefixed (default-locale) form.
105
+ *
106
+ * Each output cluster has:
107
+ * - `canonical`: the SitemapEntry that should be used as the `<url>`
108
+ * payload (default-locale variant; falls back to the first variant
109
+ * if no default-locale entry exists in the cluster).
110
+ * - `variantsByLocale`: Map of locale → SitemapEntry for the cluster.
111
+ *
112
+ * Without i18n, every entry becomes its own single-variant cluster.
113
+ *
114
+ * @internal — exported for unit testing.
115
+ */
116
+ declare function clusterPathsByLocale(entries: SitemapEntry[], i18n: I18nRoutingConfig | undefined): Cluster[];
117
+ /** A URL cluster — the canonical entry + per-locale variants. @internal */
118
+ interface Cluster {
119
+ canonical: SitemapEntry;
120
+ variantsByLocale: Map<string | null, SitemapEntry>;
121
+ }
122
+ /**
123
+ * Strip the locale prefix from a path under the i18n strategy.
124
+ *
125
+ * Returns `{ unPrefixed, locale }`:
126
+ * - `/about` under `prefix-except-default` (default=en) → `{ unPrefixed: '/about', locale: 'en' }`
127
+ * - `/de/about` under either strategy → `{ unPrefixed: '/about', locale: 'de' }`
128
+ * - `/de` (locale root) → `{ unPrefixed: '/', locale: 'de' }`
129
+ * - `/about` under `prefix` → no locale match, returns `{ unPrefixed: '/about', locale: null }`
130
+ * (the URL doesn't fit any locale subtree — sitemap treats it as standalone).
131
+ *
132
+ * @internal — exported for unit testing.
133
+ */
134
+ declare function stripLocalePrefix(path: string, locales: readonly string[], defaultLocale: string, strategy: 'prefix' | 'prefix-except-default'): {
135
+ unPrefixed: string;
136
+ locale: string | null;
137
+ };
138
+ /**
139
+ * Resolve the i18n config to feed `generateSitemap` for hreflang
140
+ * emission. Priority order:
141
+ * 1. Explicit user config — `hreflang: I18nRoutingConfig` (object)
142
+ * 2. Auto-detect from SSG manifest — `hreflang: true` + `manifestI18n`
143
+ * present (only happens in SSG mode where the manifest exists)
144
+ * 3. Nothing — emit plain sitemap without xhtml:link siblings
145
+ *
146
+ * @internal — exported for unit testing.
26
147
  */
27
- declare function generateSitemap(routeFiles: string[], config: SitemapConfig): string;
148
+ declare function resolveHreflangI18n(hreflang: boolean | I18nRoutingConfig | undefined, manifestI18n: I18nRoutingConfig | undefined): I18nRoutingConfig | undefined;
28
149
  interface RobotsConfig {
29
150
  /** Rules per user-agent. */
30
151
  rules?: RobotsRule[];
@@ -75,7 +196,10 @@ interface SeoPluginConfig {
75
196
  * pyreon(),
76
197
  * zero(),
77
198
  * seoPlugin({
78
- * sitemap: { origin: "https://example.com" },
199
+ * sitemap: {
200
+ * origin: "https://example.com",
201
+ * useSsgPaths: true, // include dynamic-route enumerations
202
+ * },
79
203
  * robots: { sitemap: "https://example.com/sitemap.xml" },
80
204
  * }),
81
205
  * ],
@@ -88,5 +212,5 @@ declare function seoPlugin(config?: SeoPluginConfig): Plugin;
88
212
  */
89
213
  declare function seoMiddleware(config?: SeoPluginConfig): Middleware;
90
214
  //#endregion
91
- export { ChangeFreq, JsonLdType, RobotsConfig, RobotsRule, SeoPluginConfig, SitemapConfig, SitemapEntry, generateRobots, generateSitemap, jsonLd, seoMiddleware, seoPlugin };
215
+ export { ChangeFreq, Cluster, JsonLdType, RobotsConfig, RobotsRule, SeoPluginConfig, SitemapConfig, SitemapEntry, clusterPathsByLocale, generateRobots, generateSitemap, jsonLd, resolveHreflangI18n, seoMiddleware, seoPlugin, stripLocalePrefix };
92
216
  //# sourceMappingURL=seo2.d.ts.map