@pyreon/zero 0.12.2 → 0.12.4

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 (145) 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 +340 -4015
  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 +310 -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/server.js +1534 -0
  34. package/lib/server.js.map +1 -0
  35. package/lib/testing.js +179 -0
  36. package/lib/testing.js.map +1 -0
  37. package/lib/theme.js +11 -2
  38. package/lib/theme.js.map +1 -1
  39. package/lib/types/actions.d.ts +27 -24
  40. package/lib/types/actions.d.ts.map +1 -1
  41. package/lib/types/ai.d.ts +76 -95
  42. package/lib/types/ai.d.ts.map +1 -1
  43. package/lib/types/api-routes.d.ts +37 -33
  44. package/lib/types/api-routes.d.ts.map +1 -1
  45. package/lib/types/cache.d.ts +26 -22
  46. package/lib/types/cache.d.ts.map +1 -1
  47. package/lib/types/client.d.ts +13 -9
  48. package/lib/types/client.d.ts.map +1 -1
  49. package/lib/types/compression.d.ts +14 -10
  50. package/lib/types/compression.d.ts.map +1 -1
  51. package/lib/types/config.d.ts +39 -4
  52. package/lib/types/config.d.ts.map +1 -1
  53. package/lib/types/cors.d.ts +20 -16
  54. package/lib/types/cors.d.ts.map +1 -1
  55. package/lib/types/csp.d.ts +42 -61
  56. package/lib/types/csp.d.ts.map +1 -1
  57. package/lib/types/env.d.ts +26 -26
  58. package/lib/types/env.d.ts.map +1 -1
  59. package/lib/types/favicon.d.ts +58 -54
  60. package/lib/types/favicon.d.ts.map +1 -1
  61. package/lib/types/font.d.ts +68 -65
  62. package/lib/types/font.d.ts.map +1 -1
  63. package/lib/types/i18n-routing.d.ts +43 -37
  64. package/lib/types/i18n-routing.d.ts.map +1 -1
  65. package/lib/types/image-plugin.d.ts +49 -45
  66. package/lib/types/image-plugin.d.ts.map +1 -1
  67. package/lib/types/image.d.ts +47 -36
  68. package/lib/types/image.d.ts.map +1 -1
  69. package/lib/types/index.d.ts +594 -56
  70. package/lib/types/index.d.ts.map +1 -1
  71. package/lib/types/link.d.ts +61 -56
  72. package/lib/types/link.d.ts.map +1 -1
  73. package/lib/types/logger.d.ts +37 -48
  74. package/lib/types/logger.d.ts.map +1 -1
  75. package/lib/types/meta.d.ts +145 -105
  76. package/lib/types/meta.d.ts.map +1 -1
  77. package/lib/types/middleware.d.ts +8 -4
  78. package/lib/types/middleware.d.ts.map +1 -1
  79. package/lib/types/og-image.d.ts +63 -59
  80. package/lib/types/og-image.d.ts.map +1 -1
  81. package/lib/types/rate-limit.d.ts +20 -16
  82. package/lib/types/rate-limit.d.ts.map +1 -1
  83. package/lib/types/script.d.ts +23 -19
  84. package/lib/types/script.d.ts.map +1 -1
  85. package/lib/types/seo.d.ts +47 -43
  86. package/lib/types/seo.d.ts.map +1 -1
  87. package/lib/types/server.d.ts +455 -0
  88. package/lib/types/server.d.ts.map +1 -0
  89. package/lib/types/testing.d.ts +64 -27
  90. package/lib/types/testing.d.ts.map +1 -1
  91. package/lib/types/theme.d.ts +22 -12
  92. package/lib/types/theme.d.ts.map +1 -1
  93. package/package.json +17 -12
  94. package/src/actions.ts +1 -3
  95. package/src/adapters/bun.ts +2 -0
  96. package/src/adapters/cloudflare.ts +2 -0
  97. package/src/adapters/netlify.ts +2 -0
  98. package/src/adapters/node.ts +2 -0
  99. package/src/adapters/validate.ts +16 -0
  100. package/src/adapters/vercel.ts +2 -0
  101. package/src/compression.ts +19 -3
  102. package/src/entry-server.ts +28 -5
  103. package/src/index.ts +20 -182
  104. package/src/link.tsx +6 -0
  105. package/src/meta.tsx +78 -16
  106. package/src/rate-limit.ts +11 -9
  107. package/src/server.ts +70 -0
  108. package/src/theme.tsx +12 -1
  109. package/src/vite-plugin.ts +5 -1
  110. package/lib/fs-router-Dil4IKZR.js +0 -290
  111. package/lib/fs-router-Dil4IKZR.js.map +0 -1
  112. package/lib/types/adapters/bun.d.ts +0 -6
  113. package/lib/types/adapters/bun.d.ts.map +0 -1
  114. package/lib/types/adapters/cloudflare.d.ts +0 -26
  115. package/lib/types/adapters/cloudflare.d.ts.map +0 -1
  116. package/lib/types/adapters/index.d.ts +0 -13
  117. package/lib/types/adapters/index.d.ts.map +0 -1
  118. package/lib/types/adapters/netlify.d.ts +0 -21
  119. package/lib/types/adapters/netlify.d.ts.map +0 -1
  120. package/lib/types/adapters/node.d.ts +0 -6
  121. package/lib/types/adapters/node.d.ts.map +0 -1
  122. package/lib/types/adapters/static.d.ts +0 -7
  123. package/lib/types/adapters/static.d.ts.map +0 -1
  124. package/lib/types/adapters/vercel.d.ts +0 -21
  125. package/lib/types/adapters/vercel.d.ts.map +0 -1
  126. package/lib/types/app.d.ts +0 -24
  127. package/lib/types/app.d.ts.map +0 -1
  128. package/lib/types/entry-server.d.ts +0 -37
  129. package/lib/types/entry-server.d.ts.map +0 -1
  130. package/lib/types/error-overlay.d.ts +0 -6
  131. package/lib/types/error-overlay.d.ts.map +0 -1
  132. package/lib/types/fs-router.d.ts +0 -47
  133. package/lib/types/fs-router.d.ts.map +0 -1
  134. package/lib/types/isr.d.ts +0 -9
  135. package/lib/types/isr.d.ts.map +0 -1
  136. package/lib/types/not-found.d.ts +0 -7
  137. package/lib/types/not-found.d.ts.map +0 -1
  138. package/lib/types/types.d.ts +0 -111
  139. package/lib/types/types.d.ts.map +0 -1
  140. package/lib/types/utils/use-intersection-observer.d.ts +0 -10
  141. package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
  142. package/lib/types/utils/with-headers.d.ts +0 -6
  143. package/lib/types/utils/with-headers.d.ts.map +0 -1
  144. package/lib/types/vite-plugin.d.ts +0 -17
  145. package/lib/types/vite-plugin.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/zero",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
5
5
  "license": "MIT",
6
6
  "author": "Vit Bokisch",
@@ -27,6 +27,11 @@
27
27
  "import": "./lib/index.js",
28
28
  "types": "./lib/types/index.d.ts"
29
29
  },
30
+ "./server": {
31
+ "bun": "./src/server.ts",
32
+ "import": "./lib/server.js",
33
+ "types": "./lib/types/server.d.ts"
34
+ },
30
35
  "./client": {
31
36
  "bun": "./src/client.ts",
32
37
  "import": "./lib/client.js",
@@ -122,7 +127,7 @@
122
127
  "import": "./lib/og-image.js",
123
128
  "types": "./lib/types/og-image.d.ts"
124
129
  },
125
- "./i18n": {
130
+ "./i18n-routing": {
126
131
  "bun": "./src/i18n-routing.ts",
127
132
  "import": "./lib/i18n-routing.js",
128
133
  "types": "./lib/types/i18n-routing.d.ts"
@@ -154,24 +159,24 @@
154
159
  }
155
160
  },
156
161
  "scripts": {
157
- "build": "vl_rolldown_build && tsc",
162
+ "build": "vl_rolldown_build",
158
163
  "dev": "vl_rolldown_build-watch",
159
164
  "test": "vitest run",
160
165
  "typecheck": "tsc --noEmit",
161
166
  "lint": "oxlint ."
162
167
  },
163
168
  "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",
169
+ "@pyreon/core": "^0.12.4",
170
+ "@pyreon/head": "^0.12.4",
171
+ "@pyreon/meta": "^0.12.4",
172
+ "@pyreon/router": "^0.12.4",
173
+ "@pyreon/runtime-dom": "^0.12.4",
174
+ "@pyreon/runtime-server": "^0.12.4",
175
+ "@pyreon/server": "^0.12.4",
176
+ "@pyreon/vite-plugin": "^0.12.4",
172
177
  "vite": "^8.0.0"
173
178
  },
174
179
  "peerDependencies": {
175
- "@pyreon/reactivity": "^0.12.2"
180
+ "@pyreon/reactivity": "^0.12.4"
176
181
  }
177
182
  }
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
@@ -1,46 +1,17 @@
1
- // ─── Core ─────────────────────────────────────────────────────────────────────
2
-
3
- export type { CreateAppOptions } from "./app";
4
- export { createApp } from "./app";
5
- export type { CreateServerOptions } from "./entry-server";
6
- export { createServer } from "./entry-server";
7
-
8
- // ─── Vite plugin ─────────────────────────────────────────────────────────────
9
-
10
- export { zeroPlugin as default } from "./vite-plugin";
11
-
12
- // ─── File-system routing ─────────────────────────────────────────────────────
13
-
14
- export type { GenerateRouteModuleOptions } from './fs-router'
15
- export {
16
- filePathToUrlPath,
17
- generateMiddlewareModule,
18
- generateRouteModule,
19
- parseFileRoutes,
20
- scanRouteFiles,
21
- } from './fs-router'
22
-
23
- // ─── Config ──────────────────────────────────────────────────────────────────
24
-
25
- export { defineConfig, resolveConfig } from "./config";
26
-
27
- // ─── ISR ─────────────────────────────────────────────────────────────────────
28
-
29
- export { createISRHandler } from "./isr";
30
-
31
- // ─── Adapters ────────────────────────────────────────────────────────────────
32
-
33
- export {
34
- bunAdapter,
35
- cloudflareAdapter,
36
- netlifyAdapter,
37
- nodeAdapter,
38
- resolveAdapter,
39
- staticAdapter,
40
- vercelAdapter,
41
- } from "./adapters";
42
-
43
- // ─── Components ─────────────────────────────────────────────────────────────
1
+ /**
2
+ * @pyreon/zero — client-safe exports.
3
+ *
4
+ * This entry contains only browser-safe components and hooks.
5
+ * No node:fs, node:path, or other server-only imports.
6
+ *
7
+ * For server/build-time features, use subpath imports:
8
+ * import { faviconPlugin } from "@pyreon/zero/favicon"
9
+ * import { createServer } from "@pyreon/zero/server"
10
+ * import { defineConfig } from "@pyreon/zero/config"
11
+ * import { validateEnv } from "@pyreon/zero/env"
12
+ */
13
+
14
+ // ─── Components (browser-safe) ──────────────────────────────────────────────
44
15
 
45
16
  export type { ImageProps, ImageSource } from "./image";
46
17
  export { Image } from "./image";
@@ -48,46 +19,16 @@ export type { LinkProps, LinkRenderProps, UseLinkReturn } from "./link";
48
19
  export { createLink, Link, prefetchRoute, useLink } from "./link";
49
20
  export type { ScriptProps, ScriptStrategy } from "./script";
50
21
  export { Script } from "./script";
22
+ export type { MetaProps } from "./meta";
23
+ export { buildMetaTags, Meta } from "./meta";
51
24
 
52
- // ─── 404 Not Found ──────────────────────────────────────────────────────────
53
-
54
- export { render404Page } from "./not-found";
55
-
56
- // ─── Middleware ──────────────────────────────────────────────────────────────
57
-
58
- export type { CacheConfig, CacheRule } from "./cache";
59
- export { cacheMiddleware, securityHeaders, varyEncoding } from "./cache";
60
- export { compose, getContext } from "./middleware";
61
-
62
- // ─── Font optimization ─────────────────────────────────────────────────────
63
-
64
- export type {
65
- FallbackMetrics,
66
- FontConfig,
67
- FontDisplay,
68
- GoogleFontInput,
69
- GoogleFontStatic,
70
- GoogleFontVariable,
71
- LocalFont,
72
- } from "./font";
73
- export { fontPlugin, fontVariables } from "./font";
74
-
75
- // ─── Image processing ──────────────────────────────────────────────────────
76
-
77
- export type {
78
- FormatSource,
79
- ImageFormat,
80
- ImagePluginConfig,
81
- ProcessedImage,
82
- } from "./image-plugin";
83
- export { imagePlugin } from "./image-plugin";
84
-
85
- // ─── Theme ──────────────────────────────────────────────────────────────────
25
+ // ─── Theme (browser-safe) ───────────────────────────────────────────────────
86
26
 
87
27
  export type { Theme } from "./theme";
88
28
  export {
89
29
  initTheme,
90
30
  resolvedTheme,
31
+ setSSRThemeDefault,
91
32
  setTheme,
92
33
  ThemeToggle,
93
34
  theme,
@@ -95,120 +36,17 @@ export {
95
36
  toggleTheme,
96
37
  } from "./theme";
97
38
 
98
- // ─── SEO ────────────────────────────────────────────────────────────────────
99
-
100
- export type {
101
- ChangeFreq,
102
- JsonLdType,
103
- RobotsConfig,
104
- RobotsRule,
105
- SeoPluginConfig,
106
- SitemapConfig,
107
- SitemapEntry,
108
- } from "./seo";
109
- export {
110
- generateRobots,
111
- generateSitemap,
112
- jsonLd,
113
- seoMiddleware,
114
- seoPlugin,
115
- } from "./seo";
116
-
117
- // ─── API routes ──────────────────────────────────────────────────────────────
118
-
119
- export type {
120
- ApiContext,
121
- ApiHandler,
122
- ApiRouteEntry,
123
- ApiRouteModule,
124
- HttpMethod,
125
- } from "./api-routes";
126
- export { createApiMiddleware, generateApiRouteModule } from "./api-routes";
127
-
128
- // ─── CORS ────────────────────────────────────────────────────────────────────
129
-
130
- export type { CorsConfig } from "./cors";
131
- export { corsMiddleware } from "./cors";
132
-
133
- // ─── Rate limiting ──────────────────────────────────────────────────────────
134
-
135
- export type { RateLimitConfig } from "./rate-limit";
136
- export { rateLimitMiddleware } from "./rate-limit";
137
-
138
- // ─── Compression ────────────────────────────────────────────────────────────
139
-
140
- export type { CompressionConfig } from "./compression";
141
- export {
142
- compressionMiddleware,
143
- compressResponse,
144
- isCompressible,
145
- } from "./compression";
146
-
147
- // ─── Actions ─────────────────────────────────────────────────────────────────
148
-
149
- export type { Action, ActionContext, ActionHandler } from "./actions";
150
- export { createActionMiddleware, defineAction } from "./actions";
151
-
152
- // ─── Favicon ────────────────────────────────────────────────────────────────
153
-
154
- export type { FaviconLocaleConfig, FaviconPluginConfig } from "./favicon";
155
- export { faviconLinks, faviconPlugin } from "./favicon";
156
-
157
- // ─── OG Image ───────────────────────────────────────────────────────────────
158
-
159
- export type {
160
- OgImageLayer,
161
- OgImagePluginConfig,
162
- OgImageTemplate,
163
- } from "./og-image";
164
- export { ogImagePath, ogImagePlugin } from "./og-image";
165
-
166
- // ─── Meta ───────────────────────────────────────────────────────────────────
167
-
168
- export type { MetaProps } from "./meta";
169
- export { buildMetaTags, Meta } from "./meta";
170
-
171
- // ─── I18n routing ───────────────────────────────────────────────────────────
39
+ // ─── I18n hooks (browser-safe) ──────────────────────────────────────────────
172
40
 
173
41
  export type { I18nRoutingConfig, LocaleContext } from "./i18n-routing";
174
42
  export {
175
43
  buildLocalePath,
176
- createLocaleContext,
177
- detectLocaleFromHeader,
178
44
  extractLocaleFromPath,
179
- i18nRouting,
180
45
  setLocale,
181
46
  useLocale,
182
47
  } from "./i18n-routing";
183
48
 
184
- // ─── CSP ────────────────────────────────────────────────────────────────────
185
-
186
- export type { CspConfig, CspDirectives } from "./csp";
187
- export { buildCspHeader, cspMiddleware, useNonce } from "./csp";
188
-
189
- // ─── Environment validation ─────────────────────────────────────────────────
190
-
191
- export type { LogEntry, LoggerConfig } from "./logger";
192
- export { loggerMiddleware } from "./logger";
193
-
194
- // ─── Request logging ────────────────────────────────────────────────────────
195
-
196
- export type { EnvValidator } from "./env";
197
- export { bool, num, oneOf, publicEnv, schema, str, url, validateEnv } from "./env";
198
-
199
- // ─── AI integration ─────────────────────────────────────────────────────────
200
-
201
- export type { AiPluginConfig, InferJsonLdOptions } from "./ai";
202
- export {
203
- aiPlugin,
204
- generateAiPluginManifest,
205
- generateLlmsFullTxt,
206
- generateLlmsTxt,
207
- generateOpenApiSpec,
208
- inferJsonLd,
209
- } from "./ai";
210
-
211
- // ─── Types ───────────────────────────────────────────────────────────────────
49
+ // ─── Types (no runtime, safe everywhere) ────────────────────────────────────
212
50
 
213
51
  export type {
214
52
  Adapter,
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,10 +1,45 @@
1
1
  import type { VNodeChild } from '@pyreon/core'
2
+ import type { UseHeadInput } from '@pyreon/head'
2
3
  import { useHead } from '@pyreon/head'
3
- import type { FaviconPluginConfig } from './favicon'
4
- import { faviconLinks } from './favicon'
5
4
  import type { I18nRoutingConfig } from './i18n-routing'
6
5
  import { extractLocaleFromPath } from './i18n-routing'
7
- import { ogImagePath } from './og-image'
6
+
7
+ // ─── Inline helpers (no node:fs dependency) ─────────────────────────────────
8
+ // These are inlined to avoid importing from favicon.ts/og-image.ts which
9
+ // pull in node:fs at the top level — making Meta unsafe for client bundles.
10
+
11
+ /** Favicon plugin config shape (type-only). */
12
+ interface FaviconPluginConfig {
13
+ source: string
14
+ themeColor?: string
15
+ manifest?: boolean
16
+ locales?: Record<string, { source: string; darkSource?: string }>
17
+ [key: string]: unknown
18
+ }
19
+
20
+ function faviconLinks(
21
+ locale: string | undefined,
22
+ config: FaviconPluginConfig,
23
+ ): Array<{ rel: string; type?: string; sizes?: string; href: string }> {
24
+ const hasLocaleOverride = locale && config.locales?.[locale]
25
+ const prefix = hasLocaleOverride ? `/${locale}` : ''
26
+ const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')
27
+ const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []
28
+ if (isSvg) links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })
29
+ links.push(
30
+ { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${prefix}/favicon-32x32.png` },
31
+ { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${prefix}/favicon-16x16.png` },
32
+ { rel: 'apple-touch-icon', sizes: '180x180', href: `${prefix}/apple-touch-icon.png` },
33
+ )
34
+ if (config.manifest !== false) links.push({ rel: 'manifest', href: `${prefix}/site.webmanifest` })
35
+ return links
36
+ }
37
+
38
+ function ogImagePath(templateName: string, locale?: string, outDir = 'og', format: 'png' | 'jpeg' = 'png'): string {
39
+ const ext = format === 'jpeg' ? 'jpg' : 'png'
40
+ const suffix = locale ? `-${locale}` : ''
41
+ return `/${outDir}/${templateName}${suffix}.${ext}`
42
+ }
8
43
 
9
44
  // ─── Meta component ────────────────────────────────────────────────────────
10
45
 
@@ -121,26 +156,53 @@ export function Meta(props: MetaProps): VNodeChild {
121
156
  // If title or description are reactive accessors, pass a getter to useHead
122
157
  // so it re-evaluates when the signals change.
123
158
  if (hasReactiveTitle || hasReactiveDescription) {
124
- useHead((() => {
159
+ useHead((): UseHeadInput => {
125
160
  const title = resolveStr(props.title)
126
161
  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)
162
+ const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]
163
+ const tags = buildMetaTags(resolved)
164
+ const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }
165
+ if (title) input.title = title
166
+ return input
167
+ })
130
168
  } else {
131
169
  const title = resolveStr(props.title)
132
170
  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)
171
+ const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]
172
+ const tags = buildMetaTags(resolved)
173
+ const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }
174
+ if (title) input.title = title
175
+ useHead(input)
135
176
  }
136
177
 
137
178
  return props.children ?? null
138
179
  }
139
180
 
181
+ interface MetaTagEntry {
182
+ name?: string
183
+ property?: string
184
+ content: string
185
+ [key: string]: string | undefined
186
+ }
187
+
188
+ interface LinkTagEntry {
189
+ rel: string
190
+ href?: string
191
+ hreflang?: string
192
+ type?: string
193
+ sizes?: string
194
+ [key: string]: string | undefined
195
+ }
196
+
197
+ interface ScriptTagEntry {
198
+ type: string
199
+ children: string
200
+ }
201
+
140
202
  interface MetaTags {
141
- meta: Array<Record<string, string>>
142
- link: Array<Record<string, string>>
143
- script: Array<{ type: string; children: string }>
203
+ meta: MetaTagEntry[]
204
+ link: LinkTagEntry[]
205
+ script: ScriptTagEntry[]
144
206
  }
145
207
 
146
208
  export function buildMetaTags(
@@ -149,9 +211,9 @@ export function buildMetaTags(
149
211
  description?: string
150
212
  },
151
213
  ): MetaTags {
152
- const meta: Array<Record<string, string>> = []
153
- const link: Array<Record<string, string>> = []
154
- const script: Array<{ type: string; children: string }> = []
214
+ const meta: MetaTagEntry[] = []
215
+ const link: LinkTagEntry[] = []
216
+ const script: ScriptTagEntry[] = []
155
217
 
156
218
  const {
157
219
  title, description, canonical, imageAlt, imageWidth, imageHeight,
@@ -280,7 +342,7 @@ export function buildMetaTags(
280
342
  if (favicon) {
281
343
  const faviconLocale = locale !== 'en_US' ? locale : undefined
282
344
  for (const fl of faviconLinks(faviconLocale, favicon)) {
283
- link.push(fl as Record<string, string>)
345
+ link.push(fl as LinkTagEntry)
284
346
  }
285
347
  // Theme color meta from favicon config
286
348
  if (favicon.themeColor) {