@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.
- package/lib/actions.js +97 -0
- package/lib/actions.js.map +1 -0
- package/lib/ai.js +503 -0
- package/lib/ai.js.map +1 -0
- package/lib/api-routes.js +137 -0
- package/lib/api-routes.js.map +1 -0
- package/lib/compression.js +80 -0
- package/lib/compression.js.map +1 -0
- package/lib/cors.js +57 -0
- package/lib/cors.js.map +1 -0
- package/lib/csp.js +119 -0
- package/lib/csp.js.map +1 -0
- package/lib/env.js +217 -0
- package/lib/env.js.map +1 -0
- package/lib/favicon.js +424 -0
- package/lib/favicon.js.map +1 -0
- package/lib/i18n-routing.js +167 -0
- package/lib/i18n-routing.js.map +1 -0
- package/lib/index.js +340 -4015
- package/lib/index.js.map +1 -1
- package/lib/link.js +5 -0
- package/lib/link.js.map +1 -1
- package/lib/logger.js +78 -0
- package/lib/logger.js.map +1 -0
- package/lib/meta.js +310 -0
- package/lib/meta.js.map +1 -0
- package/lib/middleware.js +53 -0
- package/lib/middleware.js.map +1 -0
- package/lib/og-image.js +233 -0
- package/lib/og-image.js.map +1 -0
- package/lib/rate-limit.js +76 -0
- package/lib/rate-limit.js.map +1 -0
- package/lib/server.js +1534 -0
- package/lib/server.js.map +1 -0
- package/lib/testing.js +179 -0
- package/lib/testing.js.map +1 -0
- package/lib/theme.js +11 -2
- package/lib/theme.js.map +1 -1
- package/lib/types/actions.d.ts +27 -24
- package/lib/types/actions.d.ts.map +1 -1
- package/lib/types/ai.d.ts +76 -95
- package/lib/types/ai.d.ts.map +1 -1
- package/lib/types/api-routes.d.ts +37 -33
- package/lib/types/api-routes.d.ts.map +1 -1
- package/lib/types/cache.d.ts +26 -22
- package/lib/types/cache.d.ts.map +1 -1
- package/lib/types/client.d.ts +13 -9
- package/lib/types/client.d.ts.map +1 -1
- package/lib/types/compression.d.ts +14 -10
- package/lib/types/compression.d.ts.map +1 -1
- package/lib/types/config.d.ts +39 -4
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/cors.d.ts +20 -16
- package/lib/types/cors.d.ts.map +1 -1
- package/lib/types/csp.d.ts +42 -61
- package/lib/types/csp.d.ts.map +1 -1
- package/lib/types/env.d.ts +26 -26
- package/lib/types/env.d.ts.map +1 -1
- package/lib/types/favicon.d.ts +58 -54
- package/lib/types/favicon.d.ts.map +1 -1
- package/lib/types/font.d.ts +68 -65
- package/lib/types/font.d.ts.map +1 -1
- package/lib/types/i18n-routing.d.ts +43 -37
- package/lib/types/i18n-routing.d.ts.map +1 -1
- package/lib/types/image-plugin.d.ts +49 -45
- package/lib/types/image-plugin.d.ts.map +1 -1
- package/lib/types/image.d.ts +47 -36
- package/lib/types/image.d.ts.map +1 -1
- package/lib/types/index.d.ts +594 -56
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/link.d.ts +61 -56
- package/lib/types/link.d.ts.map +1 -1
- package/lib/types/logger.d.ts +37 -48
- package/lib/types/logger.d.ts.map +1 -1
- package/lib/types/meta.d.ts +145 -105
- package/lib/types/meta.d.ts.map +1 -1
- package/lib/types/middleware.d.ts +8 -4
- package/lib/types/middleware.d.ts.map +1 -1
- package/lib/types/og-image.d.ts +63 -59
- package/lib/types/og-image.d.ts.map +1 -1
- package/lib/types/rate-limit.d.ts +20 -16
- package/lib/types/rate-limit.d.ts.map +1 -1
- package/lib/types/script.d.ts +23 -19
- package/lib/types/script.d.ts.map +1 -1
- package/lib/types/seo.d.ts +47 -43
- package/lib/types/seo.d.ts.map +1 -1
- package/lib/types/server.d.ts +455 -0
- package/lib/types/server.d.ts.map +1 -0
- package/lib/types/testing.d.ts +64 -27
- package/lib/types/testing.d.ts.map +1 -1
- package/lib/types/theme.d.ts +22 -12
- package/lib/types/theme.d.ts.map +1 -1
- package/package.json +17 -12
- package/src/actions.ts +1 -3
- package/src/adapters/bun.ts +2 -0
- package/src/adapters/cloudflare.ts +2 -0
- package/src/adapters/netlify.ts +2 -0
- package/src/adapters/node.ts +2 -0
- package/src/adapters/validate.ts +16 -0
- package/src/adapters/vercel.ts +2 -0
- package/src/compression.ts +19 -3
- package/src/entry-server.ts +28 -5
- package/src/index.ts +20 -182
- package/src/link.tsx +6 -0
- package/src/meta.tsx +78 -16
- package/src/rate-limit.ts +11 -9
- package/src/server.ts +70 -0
- package/src/theme.tsx +12 -1
- package/src/vite-plugin.ts +5 -1
- package/lib/fs-router-Dil4IKZR.js +0 -290
- package/lib/fs-router-Dil4IKZR.js.map +0 -1
- package/lib/types/adapters/bun.d.ts +0 -6
- package/lib/types/adapters/bun.d.ts.map +0 -1
- package/lib/types/adapters/cloudflare.d.ts +0 -26
- package/lib/types/adapters/cloudflare.d.ts.map +0 -1
- package/lib/types/adapters/index.d.ts +0 -13
- package/lib/types/adapters/index.d.ts.map +0 -1
- package/lib/types/adapters/netlify.d.ts +0 -21
- package/lib/types/adapters/netlify.d.ts.map +0 -1
- package/lib/types/adapters/node.d.ts +0 -6
- package/lib/types/adapters/node.d.ts.map +0 -1
- package/lib/types/adapters/static.d.ts +0 -7
- package/lib/types/adapters/static.d.ts.map +0 -1
- package/lib/types/adapters/vercel.d.ts +0 -21
- package/lib/types/adapters/vercel.d.ts.map +0 -1
- package/lib/types/app.d.ts +0 -24
- package/lib/types/app.d.ts.map +0 -1
- package/lib/types/entry-server.d.ts +0 -37
- package/lib/types/entry-server.d.ts.map +0 -1
- package/lib/types/error-overlay.d.ts +0 -6
- package/lib/types/error-overlay.d.ts.map +0 -1
- package/lib/types/fs-router.d.ts +0 -47
- package/lib/types/fs-router.d.ts.map +0 -1
- package/lib/types/isr.d.ts +0 -9
- package/lib/types/isr.d.ts.map +0 -1
- package/lib/types/not-found.d.ts +0 -7
- package/lib/types/not-found.d.ts.map +0 -1
- package/lib/types/types.d.ts +0 -111
- package/lib/types/types.d.ts.map +0 -1
- package/lib/types/utils/use-intersection-observer.d.ts +0 -10
- package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
- package/lib/types/utils/with-headers.d.ts +0 -6
- package/lib/types/utils/with-headers.d.ts.map +0 -1
- package/lib/types/vite-plugin.d.ts +0 -17
- 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.
|
|
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
|
|
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.
|
|
165
|
-
"@pyreon/head": "^0.12.
|
|
166
|
-
"@pyreon/meta": "^0.12.
|
|
167
|
-
"@pyreon/router": "^0.12.
|
|
168
|
-
"@pyreon/runtime-dom": "^0.12.
|
|
169
|
-
"@pyreon/runtime-server": "^0.12.
|
|
170
|
-
"@pyreon/server": "^0.12.
|
|
171
|
-
"@pyreon/vite-plugin": "^0.12.
|
|
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.
|
|
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_${
|
|
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 ──────────────────────────────────────────────────────────
|
package/src/adapters/bun.ts
CHANGED
|
@@ -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
|
|
package/src/adapters/netlify.ts
CHANGED
|
@@ -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
|
|
package/src/adapters/node.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/adapters/vercel.ts
CHANGED
|
@@ -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
|
|
package/src/compression.ts
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
}
|
package/src/entry-server.ts
CHANGED
|
@@ -50,19 +50,42 @@ function createRouteMiddlewareDispatcher(
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
// ───
|
|
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
|
-
// ───
|
|
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
|
-
// ───
|
|
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
|
-
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
134
|
-
|
|
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:
|
|
142
|
-
link:
|
|
143
|
-
script:
|
|
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:
|
|
153
|
-
const link:
|
|
154
|
-
const script:
|
|
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
|
|
345
|
+
link.push(fl as LinkTagEntry)
|
|
284
346
|
}
|
|
285
347
|
// Theme color meta from favicon config
|
|
286
348
|
if (favicon.themeColor) {
|