@pyreon/zero 0.12.2 → 0.12.3

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 (138) hide show
  1. package/lib/actions.js +97 -0
  2. package/lib/actions.js.map +1 -0
  3. package/lib/ai.js +503 -0
  4. package/lib/ai.js.map +1 -0
  5. package/lib/api-routes.js +137 -0
  6. package/lib/api-routes.js.map +1 -0
  7. package/lib/compression.js +80 -0
  8. package/lib/compression.js.map +1 -0
  9. package/lib/cors.js +57 -0
  10. package/lib/cors.js.map +1 -0
  11. package/lib/csp.js +119 -0
  12. package/lib/csp.js.map +1 -0
  13. package/lib/env.js +217 -0
  14. package/lib/env.js.map +1 -0
  15. package/lib/favicon.js +424 -0
  16. package/lib/favicon.js.map +1 -0
  17. package/lib/i18n-routing.js +167 -0
  18. package/lib/i18n-routing.js.map +1 -0
  19. package/lib/index.js +80 -22
  20. package/lib/index.js.map +1 -1
  21. package/lib/link.js +5 -0
  22. package/lib/link.js.map +1 -1
  23. package/lib/logger.js +78 -0
  24. package/lib/logger.js.map +1 -0
  25. package/lib/meta.js +336 -0
  26. package/lib/meta.js.map +1 -0
  27. package/lib/middleware.js +53 -0
  28. package/lib/middleware.js.map +1 -0
  29. package/lib/og-image.js +233 -0
  30. package/lib/og-image.js.map +1 -0
  31. package/lib/rate-limit.js +76 -0
  32. package/lib/rate-limit.js.map +1 -0
  33. package/lib/testing.js +179 -0
  34. package/lib/testing.js.map +1 -0
  35. package/lib/theme.js +11 -2
  36. package/lib/theme.js.map +1 -1
  37. package/lib/types/actions.d.ts +27 -24
  38. package/lib/types/actions.d.ts.map +1 -1
  39. package/lib/types/ai.d.ts +76 -95
  40. package/lib/types/ai.d.ts.map +1 -1
  41. package/lib/types/api-routes.d.ts +37 -33
  42. package/lib/types/api-routes.d.ts.map +1 -1
  43. package/lib/types/cache.d.ts +26 -22
  44. package/lib/types/cache.d.ts.map +1 -1
  45. package/lib/types/client.d.ts +13 -9
  46. package/lib/types/client.d.ts.map +1 -1
  47. package/lib/types/compression.d.ts +14 -10
  48. package/lib/types/compression.d.ts.map +1 -1
  49. package/lib/types/config.d.ts +39 -4
  50. package/lib/types/config.d.ts.map +1 -1
  51. package/lib/types/cors.d.ts +20 -16
  52. package/lib/types/cors.d.ts.map +1 -1
  53. package/lib/types/csp.d.ts +42 -61
  54. package/lib/types/csp.d.ts.map +1 -1
  55. package/lib/types/env.d.ts +26 -26
  56. package/lib/types/env.d.ts.map +1 -1
  57. package/lib/types/favicon.d.ts +58 -54
  58. package/lib/types/favicon.d.ts.map +1 -1
  59. package/lib/types/font.d.ts +68 -65
  60. package/lib/types/font.d.ts.map +1 -1
  61. package/lib/types/i18n-routing.d.ts +43 -37
  62. package/lib/types/i18n-routing.d.ts.map +1 -1
  63. package/lib/types/image-plugin.d.ts +49 -45
  64. package/lib/types/image-plugin.d.ts.map +1 -1
  65. package/lib/types/image.d.ts +47 -36
  66. package/lib/types/image.d.ts.map +1 -1
  67. package/lib/types/index.d.ts +1961 -56
  68. package/lib/types/index.d.ts.map +1 -1
  69. package/lib/types/link.d.ts +61 -56
  70. package/lib/types/link.d.ts.map +1 -1
  71. package/lib/types/logger.d.ts +37 -48
  72. package/lib/types/logger.d.ts.map +1 -1
  73. package/lib/types/meta.d.ts +180 -105
  74. package/lib/types/meta.d.ts.map +1 -1
  75. package/lib/types/middleware.d.ts +8 -4
  76. package/lib/types/middleware.d.ts.map +1 -1
  77. package/lib/types/og-image.d.ts +63 -59
  78. package/lib/types/og-image.d.ts.map +1 -1
  79. package/lib/types/rate-limit.d.ts +20 -16
  80. package/lib/types/rate-limit.d.ts.map +1 -1
  81. package/lib/types/script.d.ts +23 -19
  82. package/lib/types/script.d.ts.map +1 -1
  83. package/lib/types/seo.d.ts +47 -43
  84. package/lib/types/seo.d.ts.map +1 -1
  85. package/lib/types/testing.d.ts +64 -27
  86. package/lib/types/testing.d.ts.map +1 -1
  87. package/lib/types/theme.d.ts +22 -12
  88. package/lib/types/theme.d.ts.map +1 -1
  89. package/package.json +12 -12
  90. package/src/actions.ts +1 -3
  91. package/src/adapters/bun.ts +2 -0
  92. package/src/adapters/cloudflare.ts +2 -0
  93. package/src/adapters/netlify.ts +2 -0
  94. package/src/adapters/node.ts +2 -0
  95. package/src/adapters/validate.ts +16 -0
  96. package/src/adapters/vercel.ts +2 -0
  97. package/src/compression.ts +19 -3
  98. package/src/entry-server.ts +28 -5
  99. package/src/index.ts +1 -0
  100. package/src/link.tsx +6 -0
  101. package/src/meta.tsx +41 -13
  102. package/src/rate-limit.ts +11 -9
  103. package/src/theme.tsx +12 -1
  104. package/src/vite-plugin.ts +5 -1
  105. package/lib/types/adapters/bun.d.ts +0 -6
  106. package/lib/types/adapters/bun.d.ts.map +0 -1
  107. package/lib/types/adapters/cloudflare.d.ts +0 -26
  108. package/lib/types/adapters/cloudflare.d.ts.map +0 -1
  109. package/lib/types/adapters/index.d.ts +0 -13
  110. package/lib/types/adapters/index.d.ts.map +0 -1
  111. package/lib/types/adapters/netlify.d.ts +0 -21
  112. package/lib/types/adapters/netlify.d.ts.map +0 -1
  113. package/lib/types/adapters/node.d.ts +0 -6
  114. package/lib/types/adapters/node.d.ts.map +0 -1
  115. package/lib/types/adapters/static.d.ts +0 -7
  116. package/lib/types/adapters/static.d.ts.map +0 -1
  117. package/lib/types/adapters/vercel.d.ts +0 -21
  118. package/lib/types/adapters/vercel.d.ts.map +0 -1
  119. package/lib/types/app.d.ts +0 -24
  120. package/lib/types/app.d.ts.map +0 -1
  121. package/lib/types/entry-server.d.ts +0 -37
  122. package/lib/types/entry-server.d.ts.map +0 -1
  123. package/lib/types/error-overlay.d.ts +0 -6
  124. package/lib/types/error-overlay.d.ts.map +0 -1
  125. package/lib/types/fs-router.d.ts +0 -47
  126. package/lib/types/fs-router.d.ts.map +0 -1
  127. package/lib/types/isr.d.ts +0 -9
  128. package/lib/types/isr.d.ts.map +0 -1
  129. package/lib/types/not-found.d.ts +0 -7
  130. package/lib/types/not-found.d.ts.map +0 -1
  131. package/lib/types/types.d.ts +0 -111
  132. package/lib/types/types.d.ts.map +0 -1
  133. package/lib/types/utils/use-intersection-observer.d.ts +0 -10
  134. package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
  135. package/lib/types/utils/with-headers.d.ts +0 -6
  136. package/lib/types/utils/with-headers.d.ts.map +0 -1
  137. package/lib/types/vite-plugin.d.ts +0 -17
  138. package/lib/types/vite-plugin.d.ts.map +0 -1
@@ -1,18 +1,26 @@
1
- import type { VNodeChild } from '@pyreon/core';
2
- export type Theme = 'light' | 'dark' | 'system';
1
+ import * as _pyreon_reactivity0 from "@pyreon/reactivity";
2
+ import { VNodeChild } from "@pyreon/core";
3
+
4
+ //#region src/theme.d.ts
5
+ type Theme = 'light' | 'dark' | 'system';
3
6
  /** Reactive theme signal. */
4
- export declare const theme: import("@pyreon/reactivity").Signal<Theme>;
7
+ declare const theme: _pyreon_reactivity0.Signal<Theme>;
8
+ /**
9
+ * Set the default theme for SSR (when `matchMedia` is unavailable).
10
+ * Call once at server startup before rendering.
11
+ */
12
+ declare function setSSRThemeDefault(value: 'light' | 'dark'): void;
5
13
  /** Computed resolved theme (what's actually applied). */
6
- export declare function resolvedTheme(): 'light' | 'dark';
14
+ declare function resolvedTheme(): 'light' | 'dark';
7
15
  /** Toggle between light and dark. */
8
- export declare function toggleTheme(): void;
16
+ declare function toggleTheme(): void;
9
17
  /** Set theme explicitly. */
10
- export declare function setTheme(t: Theme): void;
18
+ declare function setTheme(t: Theme): void;
11
19
  /**
12
20
  * Initialize the theme system. Call once in your app entry or layout.
13
21
  * Reads from localStorage, listens for system preference changes.
14
22
  */
15
- export declare function initTheme(): void;
23
+ declare function initTheme(): void;
16
24
  /**
17
25
  * Theme toggle button component.
18
26
  *
@@ -20,9 +28,9 @@ export declare function initTheme(): void;
20
28
  * import { ThemeToggle } from "@pyreon/zero/theme"
21
29
  * <ThemeToggle />
22
30
  */
23
- export declare function ThemeToggle(props: {
24
- class?: string;
25
- style?: string;
31
+ declare function ThemeToggle(props: {
32
+ class?: string;
33
+ style?: string;
26
34
  }): VNodeChild;
27
35
  /**
28
36
  * Inline script to prevent flash of wrong theme.
@@ -35,5 +43,7 @@ export declare function ThemeToggle(props: {
35
43
  * ...
36
44
  * </head>
37
45
  */
38
- export 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}catch(e){}})()";
39
- //# sourceMappingURL=theme.d.ts.map
46
+ 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}catch(e){}})()";
47
+ //#endregion
48
+ export { Theme, ThemeToggle, initTheme, resolvedTheme, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme };
49
+ //# sourceMappingURL=theme2.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/theme.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAY9C,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;AAI/C,6BAA6B;AAC7B,eAAO,MAAM,KAAK,4CAA0B,CAAA;AAE5C,yDAAyD;AACzD,wBAAgB,aAAa,IAAI,OAAO,GAAG,MAAM,CAOhD;AAED,qCAAqC;AACrC,wBAAgB,WAAW,SAG1B;AAED,4BAA4B;AAC5B,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,QAUhC;AAED;;;GAGG;AACH,wBAAgB,SAAS,SAiCxB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAqDjF;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,yPAA6O,CAAA"}
1
+ {"version":3,"file":"theme2.d.ts","names":[],"sources":["../../../src/theme.tsx"],"mappings":";;;;KAYY,KAAA;;cAKC,KAAA,EAAK,mBAAA,CAAA,MAAA,CAAA,KAAA;AALlB;;;;AAAA,iBAcgB,kBAAA,CAAmB,KAAA;AATnC;AAAA,iBAcgB,aAAA,CAAA;;iBAUA,WAAA,CAAA;;iBAMA,QAAA,CAAS,CAAA,EAAG,KAAA;;;;;iBAgBZ,SAAA,CAAA;;;;;AAtBhB;;;iBAgEgB,WAAA,CAAY,KAAA;EAAS,KAAA;EAAgB,KAAA;AAAA,IAAmB,UAAA;;;;AA1CxE;;;;;AA0CA;;;cAkEa,WAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/zero",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
5
5
  "license": "MIT",
6
6
  "author": "Vit Bokisch",
@@ -122,7 +122,7 @@
122
122
  "import": "./lib/og-image.js",
123
123
  "types": "./lib/types/og-image.d.ts"
124
124
  },
125
- "./i18n": {
125
+ "./i18n-routing": {
126
126
  "bun": "./src/i18n-routing.ts",
127
127
  "import": "./lib/i18n-routing.js",
128
128
  "types": "./lib/types/i18n-routing.d.ts"
@@ -154,24 +154,24 @@
154
154
  }
155
155
  },
156
156
  "scripts": {
157
- "build": "vl_rolldown_build && tsc",
157
+ "build": "vl_rolldown_build",
158
158
  "dev": "vl_rolldown_build-watch",
159
159
  "test": "vitest run",
160
160
  "typecheck": "tsc --noEmit",
161
161
  "lint": "oxlint ."
162
162
  },
163
163
  "dependencies": {
164
- "@pyreon/core": "^0.12.2",
165
- "@pyreon/head": "^0.12.2",
166
- "@pyreon/meta": "^0.12.2",
167
- "@pyreon/router": "^0.12.2",
168
- "@pyreon/runtime-dom": "^0.12.2",
169
- "@pyreon/runtime-server": "^0.12.2",
170
- "@pyreon/server": "^0.12.2",
171
- "@pyreon/vite-plugin": "^0.12.2",
164
+ "@pyreon/core": "^0.12.3",
165
+ "@pyreon/head": "^0.12.3",
166
+ "@pyreon/meta": "^0.12.3",
167
+ "@pyreon/router": "^0.12.3",
168
+ "@pyreon/runtime-dom": "^0.12.3",
169
+ "@pyreon/runtime-server": "^0.12.3",
170
+ "@pyreon/server": "^0.12.3",
171
+ "@pyreon/vite-plugin": "^0.12.3",
172
172
  "vite": "^8.0.0"
173
173
  },
174
174
  "peerDependencies": {
175
- "@pyreon/reactivity": "^0.12.2"
175
+ "@pyreon/reactivity": "^0.12.3"
176
176
  }
177
177
  }
package/src/actions.ts CHANGED
@@ -34,7 +34,6 @@ export interface Action<T = unknown> {
34
34
  // ─── Registry ────────────────────────────────────────────────────────────────
35
35
 
36
36
  const actionRegistry = new Map<string, RegisteredAction>()
37
- let actionCounter = 0
38
37
 
39
38
  /**
40
39
  * Define a server action. Returns a callable function that:
@@ -53,7 +52,7 @@ let actionCounter = 0
53
52
  * const result = await createPost({ title: 'Hello', body: '...' })
54
53
  */
55
54
  export function defineAction<T = unknown>(handler: ActionHandler<T>): Action<T> {
56
- const id = `action_${actionCounter++}`
55
+ const id = `action_${crypto.randomUUID().slice(0, 8)}`
57
56
 
58
57
  actionRegistry.set(id, { id, handler: handler as ActionHandler })
59
58
 
@@ -100,7 +99,6 @@ export function getRegisteredActions(): Map<string, RegisteredAction> {
100
99
  */
101
100
  export function _resetActions(): void {
102
101
  actionRegistry.clear()
103
- actionCounter = 0
104
102
  }
105
103
 
106
104
  // ─── Server handler ──────────────────────────────────────────────────────────
@@ -1,4 +1,5 @@
1
1
  import type { Adapter, AdapterBuildOptions } from '../types'
2
+ import { validateBuildInputs } from './validate'
2
3
 
3
4
  /**
4
5
  * Bun adapter — generates a standalone Bun.serve() entry.
@@ -7,6 +8,7 @@ export function bunAdapter(): Adapter {
7
8
  return {
8
9
  name: 'bun',
9
10
  async build(options: AdapterBuildOptions) {
11
+ await validateBuildInputs(options)
10
12
  const { writeFile, cp, mkdir } = await import('node:fs/promises')
11
13
  const { join } = await import('node:path')
12
14
 
@@ -1,4 +1,5 @@
1
1
  import type { Adapter, AdapterBuildOptions } from '../types'
2
+ import { validateBuildInputs } from './validate'
2
3
 
3
4
  /**
4
5
  * Cloudflare Pages adapter — generates output for Cloudflare Pages with Functions.
@@ -27,6 +28,7 @@ export function cloudflareAdapter(): Adapter {
27
28
  return {
28
29
  name: 'cloudflare',
29
30
  async build(options: AdapterBuildOptions) {
31
+ await validateBuildInputs(options)
30
32
  const { writeFile, cp, mkdir } = await import('node:fs/promises')
31
33
  const { join } = await import('node:path')
32
34
 
@@ -1,4 +1,5 @@
1
1
  import type { Adapter, AdapterBuildOptions } from '../types'
2
+ import { validateBuildInputs } from './validate'
2
3
 
3
4
  /**
4
5
  * Netlify adapter — generates output for Netlify Functions (v2).
@@ -22,6 +23,7 @@ export function netlifyAdapter(): Adapter {
22
23
  return {
23
24
  name: 'netlify',
24
25
  async build(options: AdapterBuildOptions) {
26
+ await validateBuildInputs(options)
25
27
  const { writeFile, cp, mkdir } = await import('node:fs/promises')
26
28
  const { join } = await import('node:path')
27
29
 
@@ -1,4 +1,5 @@
1
1
  import type { Adapter, AdapterBuildOptions } from '../types'
2
+ import { validateBuildInputs } from './validate'
2
3
 
3
4
  /**
4
5
  * Node.js adapter — generates a standalone server entry using node:http.
@@ -7,6 +8,7 @@ export function nodeAdapter(): Adapter {
7
8
  return {
8
9
  name: 'node',
9
10
  async build(options: AdapterBuildOptions) {
11
+ await validateBuildInputs(options)
10
12
  const { writeFile, cp, mkdir } = await import('node:fs/promises')
11
13
  const { join } = await import('node:path')
12
14
 
@@ -0,0 +1,16 @@
1
+ import type { AdapterBuildOptions } from '../types'
2
+
3
+ /**
4
+ * Validate that adapter build inputs exist before copying.
5
+ * Throws with a clear error message if directories are missing.
6
+ * @internal
7
+ */
8
+ export async function validateBuildInputs(options: AdapterBuildOptions): Promise<void> {
9
+ const { existsSync } = await import('node:fs')
10
+ if (!existsSync(options.clientOutDir)) {
11
+ throw new Error(`[zero:adapter] Client build output not found: ${options.clientOutDir}. Run "vite build" first.`)
12
+ }
13
+ if (!existsSync(options.serverEntry)) {
14
+ throw new Error(`[zero:adapter] Server entry not found: ${options.serverEntry}. Run "vite build --ssr" first.`)
15
+ }
16
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Adapter, AdapterBuildOptions } from '../types'
2
+ import { validateBuildInputs } from './validate'
2
3
 
3
4
  /**
4
5
  * Vercel adapter — generates output for Vercel's Build Output API v3.
@@ -22,6 +23,7 @@ export function vercelAdapter(): Adapter {
22
23
  return {
23
24
  name: 'vercel',
24
25
  async build(options: AdapterBuildOptions) {
26
+ await validateBuildInputs(options)
25
27
  const { writeFile, cp, mkdir } = await import('node:fs/promises')
26
28
  const { join } = await import('node:path')
27
29
 
@@ -94,7 +94,23 @@ export function isCompressible(contentType: string): boolean {
94
94
  }
95
95
 
96
96
  async function compress(data: ArrayBuffer, encoding: 'gzip' | 'deflate'): Promise<ArrayBuffer> {
97
- const format = encoding === 'gzip' ? 'gzip' : 'deflate'
98
- const stream = new Blob([data]).stream().pipeThrough(new CompressionStream(format))
99
- return new Response(stream).arrayBuffer()
97
+ // CompressionStream is available in modern browsers and Node 18+/Bun.
98
+ // Fallback: try node:zlib for older runtimes.
99
+ if (typeof CompressionStream !== 'undefined') {
100
+ const format = encoding === 'gzip' ? 'gzip' : 'deflate'
101
+ const stream = new Blob([data]).stream().pipeThrough(new CompressionStream(format))
102
+ return new Response(stream).arrayBuffer()
103
+ }
104
+
105
+ // Node.js fallback via zlib
106
+ try {
107
+ const zlib = await import('node:zlib')
108
+ const { promisify } = await import('node:util')
109
+ const fn = encoding === 'gzip' ? promisify(zlib.gzip) : promisify(zlib.deflate)
110
+ const result = await fn(Buffer.from(data))
111
+ return result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength)
112
+ } catch {
113
+ // No compression available — return uncompressed
114
+ return data
115
+ }
100
116
  }
@@ -50,19 +50,42 @@ function createRouteMiddlewareDispatcher(
50
50
  };
51
51
  }
52
52
 
53
- /** Simple URL pattern matcher supporting :param and :param* segments. */
53
+ /**
54
+ * URL pattern matcher supporting :param and :param* segments.
55
+ *
56
+ * Rules:
57
+ * - Static segments must match exactly
58
+ * - `:param` matches a single path segment
59
+ * - `:param*` matches all remaining segments (must be last, and path must
60
+ * have matched all preceding segments)
61
+ * - Path length must match pattern length (unless catch-all)
62
+ */
54
63
  export function matchPattern(pattern: string, path: string): boolean {
55
64
  const patternParts = pattern.split("/").filter(Boolean);
56
65
  const pathParts = path.split("/").filter(Boolean);
57
66
 
58
67
  for (let i = 0; i < patternParts.length; i++) {
59
- const pp = patternParts[i];
60
- if (!pp) continue;
61
- if (pp.endsWith("*")) return true; // catch-all matches everything after
62
- if (pp.startsWith(":")) continue; // dynamic segment matches anything
68
+ const pp = patternParts[i]!;
69
+
70
+ // Catch-all: matches remaining segments, but only if we've matched
71
+ // all preceding segments up to this point
72
+ if (pp.endsWith("*")) {
73
+ // All segments before the catch-all must have matched (we got here)
74
+ // and there must be at least one remaining path segment
75
+ return i <= pathParts.length;
76
+ }
77
+
78
+ // No more path segments to match against
79
+ if (i >= pathParts.length) return false;
80
+
81
+ // Dynamic segment matches any single segment
82
+ if (pp.startsWith(":")) continue;
83
+
84
+ // Static segment must match exactly
63
85
  if (pp !== pathParts[i]) return false;
64
86
  }
65
87
 
88
+ // All pattern parts consumed — path must also be fully consumed
66
89
  return patternParts.length === pathParts.length;
67
90
  }
68
91
 
package/src/index.ts CHANGED
@@ -88,6 +88,7 @@ export type { Theme } from "./theme";
88
88
  export {
89
89
  initTheme,
90
90
  resolvedTheme,
91
+ setSSRThemeDefault,
91
92
  setTheme,
92
93
  ThemeToggle,
93
94
  theme,
package/src/link.tsx CHANGED
@@ -70,10 +70,16 @@ export interface UseLinkReturn {
70
70
  classes: () => string
71
71
  }
72
72
 
73
+ const MAX_PREFETCH_CACHE = 200
73
74
  const prefetched = new Set<string>()
74
75
 
75
76
  function doPrefetch(href: string) {
76
77
  if (prefetched.has(href)) return
78
+ // Evict oldest entries when cache is full
79
+ if (prefetched.size >= MAX_PREFETCH_CACHE) {
80
+ const first = prefetched.values().next().value
81
+ if (first) prefetched.delete(first)
82
+ }
77
83
  prefetched.add(href)
78
84
 
79
85
  const docLink = document.createElement('link')
package/src/meta.tsx CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { VNodeChild } from '@pyreon/core'
2
+ import type { UseHeadInput } from '@pyreon/head'
2
3
  import { useHead } from '@pyreon/head'
3
4
  import type { FaviconPluginConfig } from './favicon'
4
5
  import { faviconLinks } from './favicon'
@@ -121,26 +122,53 @@ export function Meta(props: MetaProps): VNodeChild {
121
122
  // If title or description are reactive accessors, pass a getter to useHead
122
123
  // so it re-evaluates when the signals change.
123
124
  if (hasReactiveTitle || hasReactiveDescription) {
124
- useHead((() => {
125
+ useHead((): UseHeadInput => {
125
126
  const title = resolveStr(props.title)
126
127
  const description = resolveStr(props.description)
127
- const tags = buildMetaTags({ ...props, title, description } as any)
128
- return { title, meta: tags.meta, link: tags.link, script: tags.script }
129
- }) as any)
128
+ const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]
129
+ const tags = buildMetaTags(resolved)
130
+ const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }
131
+ if (title) input.title = title
132
+ return input
133
+ })
130
134
  } else {
131
135
  const title = resolveStr(props.title)
132
136
  const description = resolveStr(props.description)
133
- const tags = buildMetaTags({ ...props, title, description } as any)
134
- useHead({ title, meta: tags.meta, link: tags.link, script: tags.script } as any)
137
+ const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]
138
+ const tags = buildMetaTags(resolved)
139
+ const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }
140
+ if (title) input.title = title
141
+ useHead(input)
135
142
  }
136
143
 
137
144
  return props.children ?? null
138
145
  }
139
146
 
147
+ interface MetaTagEntry {
148
+ name?: string
149
+ property?: string
150
+ content: string
151
+ [key: string]: string | undefined
152
+ }
153
+
154
+ interface LinkTagEntry {
155
+ rel: string
156
+ href?: string
157
+ hreflang?: string
158
+ type?: string
159
+ sizes?: string
160
+ [key: string]: string | undefined
161
+ }
162
+
163
+ interface ScriptTagEntry {
164
+ type: string
165
+ children: string
166
+ }
167
+
140
168
  interface MetaTags {
141
- meta: Array<Record<string, string>>
142
- link: Array<Record<string, string>>
143
- script: Array<{ type: string; children: string }>
169
+ meta: MetaTagEntry[]
170
+ link: LinkTagEntry[]
171
+ script: ScriptTagEntry[]
144
172
  }
145
173
 
146
174
  export function buildMetaTags(
@@ -149,9 +177,9 @@ export function buildMetaTags(
149
177
  description?: string
150
178
  },
151
179
  ): MetaTags {
152
- const meta: Array<Record<string, string>> = []
153
- const link: Array<Record<string, string>> = []
154
- const script: Array<{ type: string; children: string }> = []
180
+ const meta: MetaTagEntry[] = []
181
+ const link: LinkTagEntry[] = []
182
+ const script: ScriptTagEntry[] = []
155
183
 
156
184
  const {
157
185
  title, description, canonical, imageAlt, imageWidth, imageHeight,
@@ -280,7 +308,7 @@ export function buildMetaTags(
280
308
  if (favicon) {
281
309
  const faviconLocale = locale !== 'en_US' ? locale : undefined
282
310
  for (const fl of faviconLinks(faviconLocale, favicon)) {
283
- link.push(fl as Record<string, string>)
311
+ link.push(fl as LinkTagEntry)
284
312
  }
285
313
  // Theme color meta from favicon config
286
314
  if (favicon.themeColor) {
package/src/rate-limit.ts CHANGED
@@ -51,18 +51,17 @@ export function rateLimitMiddleware(config: RateLimitConfig = {}): Middleware {
51
51
 
52
52
  const windowMs = windowSec * 1000
53
53
  const store = new Map<string, RateLimitEntry>()
54
-
55
- // Periodic cleanup of expired entries
56
- const cleanupInterval = setInterval(() => {
57
- const now = Date.now()
54
+ const MAX_STORE_SIZE = 10000
55
+ let lastCleanup = Date.now()
56
+
57
+ // Inline cleanup — runs during request processing, no setInterval needed.
58
+ // Evicts expired entries when store exceeds half capacity or on window boundary.
59
+ function cleanupIfNeeded(now: number) {
60
+ if (store.size < MAX_STORE_SIZE / 2 && now - lastCleanup < windowMs) return
61
+ lastCleanup = now
58
62
  for (const [key, entry] of store) {
59
63
  if (entry.resetAt <= now) store.delete(key)
60
64
  }
61
- }, windowMs)
62
-
63
- // Allow GC to clean up the interval
64
- if (typeof cleanupInterval === 'object' && 'unref' in cleanupInterval) {
65
- cleanupInterval.unref()
66
65
  }
67
66
 
68
67
  return (ctx: MiddlewareContext) => {
@@ -72,6 +71,9 @@ export function rateLimitMiddleware(config: RateLimitConfig = {}): Middleware {
72
71
 
73
72
  const key = keyFn(ctx)
74
73
  const now = Date.now()
74
+
75
+ cleanupIfNeeded(now)
76
+
75
77
  let entry = store.get(key)
76
78
 
77
79
  if (!entry || entry.resetAt <= now) {
package/src/theme.tsx CHANGED
@@ -17,11 +17,22 @@ const STORAGE_KEY = 'zero-theme'
17
17
  /** Reactive theme signal. */
18
18
  export const theme = signal<Theme>('system')
19
19
 
20
+ /** SSR fallback when system preference can't be detected. Default: 'light'. */
21
+ let _ssrDefault: 'light' | 'dark' = 'light'
22
+
23
+ /**
24
+ * Set the default theme for SSR (when `matchMedia` is unavailable).
25
+ * Call once at server startup before rendering.
26
+ */
27
+ export function setSSRThemeDefault(value: 'light' | 'dark'): void {
28
+ _ssrDefault = value
29
+ }
30
+
20
31
  /** Computed resolved theme (what's actually applied). */
21
32
  export function resolvedTheme(): 'light' | 'dark' {
22
33
  const t = theme()
23
34
  if (t === 'system') {
24
- if (typeof window === 'undefined') return 'dark'
35
+ if (typeof window === 'undefined') return _ssrDefault
25
36
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
26
37
  }
27
38
  return t
@@ -126,7 +126,11 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin {
126
126
  (handled) => {
127
127
  if (!handled) next();
128
128
  },
129
- () => next(), // On error, fall through to Vite's default handling
129
+ (err) => {
130
+ // oxlint-disable-next-line no-console
131
+ console.error('[zero] Error in 404 handler:', err);
132
+ next();
133
+ },
130
134
  );
131
135
  });
132
136
 
@@ -1,6 +0,0 @@
1
- import type { Adapter } from '../types';
2
- /**
3
- * Bun adapter — generates a standalone Bun.serve() entry.
4
- */
5
- export declare function bunAdapter(): Adapter;
6
- //# sourceMappingURL=bun.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bun.d.ts","sourceRoot":"","sources":["../../../src/adapters/bun.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAuB,MAAM,UAAU,CAAA;AAE5D;;GAEG;AACH,wBAAgB,UAAU,IAAI,OAAO,CA2DpC"}
@@ -1,26 +0,0 @@
1
- import type { Adapter } from '../types';
2
- /**
3
- * Cloudflare Pages adapter — generates output for Cloudflare Pages with Functions.
4
- *
5
- * Produces:
6
- * - Client assets in the output directory root (served as static)
7
- * - `_worker.js` — Cloudflare Pages Function for SSR
8
- *
9
- * Note: Cloudflare Pages Functions have a ~1MB module size limit.
10
- * For large apps, configure Vite's SSR build to bundle server code:
11
- * `ssr: { noExternal: true }` in vite.config.ts.
12
- *
13
- * Deploy with: `npx wrangler pages deploy ./dist`
14
- *
15
- * @example
16
- * ```ts
17
- * // zero.config.ts
18
- * import { defineConfig } from "@pyreon/zero"
19
- *
20
- * export default defineConfig({
21
- * adapter: "cloudflare",
22
- * })
23
- * ```
24
- */
25
- export declare function cloudflareAdapter(): Adapter;
26
- //# sourceMappingURL=cloudflare.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../../src/adapters/cloudflare.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAuB,MAAM,UAAU,CAAA;AAE5D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAwD3C"}
@@ -1,13 +0,0 @@
1
- export { bunAdapter } from './bun';
2
- export { cloudflareAdapter } from './cloudflare';
3
- export { netlifyAdapter } from './netlify';
4
- export { nodeAdapter } from './node';
5
- export { staticAdapter } from './static';
6
- export { vercelAdapter } from './vercel';
7
- import type { Adapter, ZeroConfig } from '../types';
8
- /**
9
- * Resolve the adapter from config.
10
- * Returns a built-in adapter or throws if unknown.
11
- */
12
- export declare function resolveAdapter(config: ZeroConfig): Adapter;
13
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAQnD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAmB1D"}
@@ -1,21 +0,0 @@
1
- import type { Adapter } from '../types';
2
- /**
3
- * Netlify adapter — generates output for Netlify Functions (v2).
4
- *
5
- * Produces:
6
- * - Client assets in `publish/` directory
7
- * - `netlify/functions/ssr.mjs` — Netlify Function for SSR
8
- * - `netlify.toml` — routing configuration
9
- *
10
- * @example
11
- * ```ts
12
- * // zero.config.ts
13
- * import { defineConfig } from "@pyreon/zero"
14
- *
15
- * export default defineConfig({
16
- * adapter: "netlify",
17
- * })
18
- * ```
19
- */
20
- export declare function netlifyAdapter(): Adapter;
21
- //# sourceMappingURL=netlify.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"netlify.d.ts","sourceRoot":"","sources":["../../../src/adapters/netlify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAuB,MAAM,UAAU,CAAA;AAE5D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,IAAI,OAAO,CA+DxC"}
@@ -1,6 +0,0 @@
1
- import type { Adapter } from '../types';
2
- /**
3
- * Node.js adapter — generates a standalone server entry using node:http.
4
- */
5
- export declare function nodeAdapter(): Adapter;
6
- //# sourceMappingURL=node.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../../src/adapters/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAuB,MAAM,UAAU,CAAA;AAE5D;;GAEG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAwGrC"}
@@ -1,7 +0,0 @@
1
- import type { Adapter } from '../types';
2
- /**
3
- * Static adapter — just copies the client build output.
4
- * Used with SSG mode where all pages are pre-rendered at build time.
5
- */
6
- export declare function staticAdapter(): Adapter;
7
- //# sourceMappingURL=static.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../../src/adapters/static.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAuB,MAAM,UAAU,CAAA;AAE5D;;;GAGG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAUvC"}
@@ -1,21 +0,0 @@
1
- import type { Adapter } from '../types';
2
- /**
3
- * Vercel adapter — generates output for Vercel's Build Output API v3.
4
- *
5
- * Produces a `.vercel/output` directory with:
6
- * - `static/` — client-side assets (JS, CSS, images)
7
- * - `functions/ssr.func/` — serverless function for SSR
8
- * - `config.json` — routing configuration
9
- *
10
- * @example
11
- * ```ts
12
- * // zero.config.ts
13
- * import { defineConfig } from "@pyreon/zero"
14
- *
15
- * export default defineConfig({
16
- * adapter: "vercel",
17
- * })
18
- * ```
19
- */
20
- export declare function vercelAdapter(): Adapter;
21
- //# sourceMappingURL=vercel.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../../src/adapters/vercel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAuB,MAAM,UAAU,CAAA;AAE5D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,IAAI,OAAO,CA+DvC"}