@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.
- 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 +80 -22
- 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 +336 -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/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 +1961 -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 +180 -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/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 +12 -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 +1 -0
- package/src/link.tsx +6 -0
- package/src/meta.tsx +41 -13
- package/src/rate-limit.ts +11 -9
- package/src/theme.tsx +12 -1
- package/src/vite-plugin.ts +5 -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/lib/types/theme.d.ts
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
declare function resolvedTheme(): 'light' | 'dark';
|
|
7
15
|
/** Toggle between light and dark. */
|
|
8
|
-
|
|
16
|
+
declare function toggleTheme(): void;
|
|
9
17
|
/** Set theme explicitly. */
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
39
|
-
//#
|
|
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
|
package/lib/types/theme.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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_${
|
|
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
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
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
134
|
-
|
|
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:
|
|
142
|
-
link:
|
|
143
|
-
script:
|
|
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:
|
|
153
|
-
const link:
|
|
154
|
-
const script:
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
package/src/vite-plugin.ts
CHANGED
|
@@ -126,7 +126,11 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin {
|
|
|
126
126
|
(handled) => {
|
|
127
127
|
if (!handled) next();
|
|
128
128
|
},
|
|
129
|
-
() =>
|
|
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 +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 +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"}
|