@pyreon/zero 0.4.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/lib/cache.js.map +1 -1
  2. package/lib/client.js.map +1 -1
  3. package/lib/config.js.map +1 -1
  4. package/lib/font.js.map +1 -1
  5. package/lib/fs-router-BkbIWqek.js.map +1 -1
  6. package/lib/fs-router-n4VA4lxu.js.map +1 -1
  7. package/lib/image-plugin.js.map +1 -1
  8. package/lib/image.js +5 -5
  9. package/lib/image.js.map +1 -1
  10. package/lib/index.js +14 -14
  11. package/lib/index.js.map +1 -1
  12. package/lib/link.js +9 -9
  13. package/lib/link.js.map +1 -1
  14. package/lib/script.js +1 -1
  15. package/lib/script.js.map +1 -1
  16. package/lib/seo.js.map +1 -1
  17. package/lib/theme.js +2 -2
  18. package/lib/theme.js.map +1 -1
  19. package/package.json +14 -13
  20. package/src/actions.ts +20 -28
  21. package/src/adapters/bun.ts +7 -7
  22. package/src/adapters/index.ts +12 -14
  23. package/src/adapters/node.ts +8 -11
  24. package/src/adapters/static.ts +3 -3
  25. package/src/api-routes.ts +23 -50
  26. package/src/app.ts +9 -13
  27. package/src/cache.ts +16 -29
  28. package/src/client.ts +8 -8
  29. package/src/compression.ts +21 -28
  30. package/src/config.ts +6 -7
  31. package/src/cors.ts +20 -28
  32. package/src/entry-server.ts +15 -19
  33. package/src/error-overlay.ts +10 -13
  34. package/src/font.ts +44 -55
  35. package/src/fs-router.ts +44 -63
  36. package/src/image-plugin.ts +53 -79
  37. package/src/image.tsx +41 -43
  38. package/src/index.ts +36 -36
  39. package/src/isr.ts +8 -8
  40. package/src/link.tsx +35 -38
  41. package/src/rate-limit.ts +15 -15
  42. package/src/script.tsx +21 -22
  43. package/src/seo.ts +47 -57
  44. package/src/sharp.d.ts +2 -6
  45. package/src/testing.ts +8 -12
  46. package/src/theme.tsx +19 -21
  47. package/src/types.ts +6 -6
  48. package/src/utils/use-intersection-observer.ts +2 -2
  49. package/src/utils/with-headers.ts +1 -4
  50. package/src/vite-plugin.ts +21 -28
  51. package/lib/types/actions.d.ts +0 -57
  52. package/lib/types/actions.d.ts.map +0 -1
  53. package/lib/types/adapters/bun.d.ts +0 -6
  54. package/lib/types/adapters/bun.d.ts.map +0 -1
  55. package/lib/types/adapters/index.d.ts +0 -10
  56. package/lib/types/adapters/index.d.ts.map +0 -1
  57. package/lib/types/adapters/node.d.ts +0 -6
  58. package/lib/types/adapters/node.d.ts.map +0 -1
  59. package/lib/types/adapters/static.d.ts +0 -7
  60. package/lib/types/adapters/static.d.ts.map +0 -1
  61. package/lib/types/api-routes.d.ts +0 -66
  62. package/lib/types/api-routes.d.ts.map +0 -1
  63. package/lib/types/app.d.ts +0 -24
  64. package/lib/types/app.d.ts.map +0 -1
  65. package/lib/types/cache.d.ts +0 -54
  66. package/lib/types/cache.d.ts.map +0 -1
  67. package/lib/types/client.d.ts +0 -19
  68. package/lib/types/client.d.ts.map +0 -1
  69. package/lib/types/compression.d.ts +0 -33
  70. package/lib/types/compression.d.ts.map +0 -1
  71. package/lib/types/config.d.ts +0 -18
  72. package/lib/types/config.d.ts.map +0 -1
  73. package/lib/types/cors.d.ts +0 -32
  74. package/lib/types/cors.d.ts.map +0 -1
  75. package/lib/types/entry-server.d.ts +0 -34
  76. package/lib/types/entry-server.d.ts.map +0 -1
  77. package/lib/types/error-overlay.d.ts +0 -6
  78. package/lib/types/error-overlay.d.ts.map +0 -1
  79. package/lib/types/font.d.ts +0 -119
  80. package/lib/types/font.d.ts.map +0 -1
  81. package/lib/types/fs-router.d.ts +0 -38
  82. package/lib/types/fs-router.d.ts.map +0 -1
  83. package/lib/types/image-plugin.d.ts +0 -79
  84. package/lib/types/image-plugin.d.ts.map +0 -1
  85. package/lib/types/image.d.ts +0 -51
  86. package/lib/types/image.d.ts.map +0 -1
  87. package/lib/types/index.d.ts +0 -37
  88. package/lib/types/index.d.ts.map +0 -1
  89. package/lib/types/isr.d.ts +0 -9
  90. package/lib/types/isr.d.ts.map +0 -1
  91. package/lib/types/link.d.ts +0 -116
  92. package/lib/types/link.d.ts.map +0 -1
  93. package/lib/types/rate-limit.d.ts +0 -34
  94. package/lib/types/rate-limit.d.ts.map +0 -1
  95. package/lib/types/script.d.ts +0 -35
  96. package/lib/types/script.d.ts.map +0 -1
  97. package/lib/types/seo.d.ts +0 -88
  98. package/lib/types/seo.d.ts.map +0 -1
  99. package/lib/types/testing.d.ts +0 -85
  100. package/lib/types/testing.d.ts.map +0 -1
  101. package/lib/types/theme.d.ts +0 -39
  102. package/lib/types/theme.d.ts.map +0 -1
  103. package/lib/types/types.d.ts +0 -109
  104. package/lib/types/types.d.ts.map +0 -1
  105. package/lib/types/utils/use-intersection-observer.d.ts +0 -10
  106. package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
  107. package/lib/types/utils/with-headers.d.ts +0 -6
  108. package/lib/types/utils/with-headers.d.ts.map +0 -1
  109. package/lib/types/vite-plugin.d.ts +0 -17
  110. package/lib/types/vite-plugin.d.ts.map +0 -1
package/src/isr.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ISRConfig } from './types'
1
+ import type { ISRConfig } from "./types"
2
2
 
3
3
  // ─── ISR Cache ───────────────────────────────────────────────────────────────
4
4
 
@@ -28,7 +28,7 @@ export function createISRHandler(
28
28
  revalidating.add(key)
29
29
 
30
30
  try {
31
- const req = new Request(url.href, { method: 'GET' })
31
+ const req = new Request(url.href, { method: "GET" })
32
32
  const res = await handler(req)
33
33
  const html = await res.text()
34
34
  const headers: Record<string, string> = {}
@@ -46,7 +46,7 @@ export function createISRHandler(
46
46
 
47
47
  return async (req: Request): Promise<Response> => {
48
48
  // Only cache GET requests
49
- if (req.method !== 'GET') {
49
+ if (req.method !== "GET") {
50
50
  return handler(req)
51
51
  }
52
52
 
@@ -66,9 +66,9 @@ export function createISRHandler(
66
66
  status: 200,
67
67
  headers: {
68
68
  ...entry.headers,
69
- 'content-type': 'text/html; charset=utf-8',
70
- 'x-isr-cache': age > revalidateMs ? 'STALE' : 'HIT',
71
- 'x-isr-age': String(Math.round(age / 1000)),
69
+ "content-type": "text/html; charset=utf-8",
70
+ "x-isr-cache": age > revalidateMs ? "STALE" : "HIT",
71
+ "x-isr-age": String(Math.round(age / 1000)),
72
72
  },
73
73
  })
74
74
  }
@@ -87,8 +87,8 @@ export function createISRHandler(
87
87
  status: 200,
88
88
  headers: {
89
89
  ...headers,
90
- 'content-type': 'text/html; charset=utf-8',
91
- 'x-isr-cache': 'MISS',
90
+ "content-type": "text/html; charset=utf-8",
91
+ "x-isr-cache": "MISS",
92
92
  },
93
93
  })
94
94
  }
package/src/link.tsx CHANGED
@@ -1,6 +1,6 @@
1
- import { createRef } from '@pyreon/core'
2
- import { useRouter } from '@pyreon/router'
3
- import { useIntersectionObserver } from './utils/use-intersection-observer'
1
+ import { createRef } from "@pyreon/core"
2
+ import { useRouter } from "@pyreon/router"
3
+ import { useIntersectionObserver } from "./utils/use-intersection-observer"
4
4
 
5
5
  // ─── Link component with prefetching ────────────────────────────────────────
6
6
  //
@@ -23,19 +23,19 @@ export interface LinkProps {
23
23
  /** Class applied when this link exactly matches the current route. */
24
24
  exactActiveClass?: string
25
25
  /** Prefetch strategy. Default: "hover" */
26
- prefetch?: 'hover' | 'viewport' | 'none'
26
+ prefetch?: "hover" | "viewport" | "none"
27
27
  /** Open in new tab. */
28
28
  external?: boolean
29
29
  /** Inline styles. */
30
30
  style?: string
31
31
  /** ARIA label. */
32
- 'aria-label'?: string
32
+ "aria-label"?: string
33
33
  }
34
34
 
35
35
  /** Props passed to a custom component via createLink. */
36
36
  export interface LinkRenderProps {
37
37
  href: string
38
- ref: import('@pyreon/core').Ref<HTMLElement>
38
+ ref: import("@pyreon/core").Ref<HTMLAnchorElement>
39
39
  onClick: (e: MouseEvent) => void
40
40
  onMouseEnter: () => void
41
41
  onTouchStart: () => void
@@ -46,14 +46,14 @@ export interface LinkRenderProps {
46
46
  style?: string
47
47
  target?: string
48
48
  rel?: string
49
- 'aria-label'?: string
49
+ "aria-label"?: string
50
50
  children?: any
51
51
  }
52
52
 
53
53
  /** Return type of useLink. */
54
54
  export interface UseLinkReturn {
55
55
  /** Ref object — attach to the root element for viewport-based prefetch. */
56
- ref: import('@pyreon/core').Ref<HTMLElement>
56
+ ref: import("@pyreon/core").Ref<HTMLAnchorElement>
57
57
  /** Click handler — performs client-side navigation. */
58
58
  handleClick: (e: MouseEvent) => void
59
59
  /** Mouse enter handler — triggers hover prefetch. */
@@ -74,15 +74,15 @@ function doPrefetch(href: string) {
74
74
  if (prefetched.has(href)) return
75
75
  prefetched.add(href)
76
76
 
77
- const docLink = document.createElement('link')
78
- docLink.rel = 'prefetch'
77
+ const docLink = document.createElement("link")
78
+ docLink.rel = "prefetch"
79
79
  docLink.href = href
80
- docLink.as = 'document'
80
+ docLink.as = "document"
81
81
  document.head.appendChild(docLink)
82
82
 
83
83
  try {
84
- const chunkHint = document.createElement('link')
85
- chunkHint.rel = 'modulepreload'
84
+ const chunkHint = document.createElement("link")
85
+ chunkHint.rel = "modulepreload"
86
86
  chunkHint.href = href
87
87
  document.head.appendChild(chunkHint)
88
88
  } catch {
@@ -100,7 +100,7 @@ function doPrefetch(href: string) {
100
100
  * function MyLink(props: LinkProps) {
101
101
  * const link = useLink(props)
102
102
  * return (
103
- * <button ref={link.ref} class={link.classes()} onclick={link.handleClick}>
103
+ * <button ref={link.ref} class={link.classes()} onClick={link.handleClick}>
104
104
  * {props.children}
105
105
  * </button>
106
106
  * )
@@ -108,8 +108,8 @@ function doPrefetch(href: string) {
108
108
  */
109
109
  export function useLink(props: LinkProps): UseLinkReturn {
110
110
  const router = useRouter()
111
- const elementRef = createRef<HTMLElement>()
112
- const strategy = props.prefetch ?? 'hover'
111
+ const elementRef = createRef<HTMLAnchorElement>()
112
+ const strategy = props.prefetch ?? "hover"
113
113
 
114
114
  function handleClick(e: MouseEvent) {
115
115
  if (
@@ -128,18 +128,18 @@ export function useLink(props: LinkProps): UseLinkReturn {
128
128
  }
129
129
 
130
130
  function handleMouseEnter() {
131
- if (strategy === 'hover') {
131
+ if (strategy === "hover") {
132
132
  doPrefetch(props.href)
133
133
  }
134
134
  }
135
135
 
136
136
  function handleTouchStart() {
137
- if (strategy === 'hover' || strategy === 'viewport') {
137
+ if (strategy === "hover" || strategy === "viewport") {
138
138
  doPrefetch(props.href)
139
139
  }
140
140
  }
141
141
 
142
- if (strategy === 'viewport') {
142
+ if (strategy === "viewport") {
143
143
  useIntersectionObserver(
144
144
  () => elementRef.current ?? undefined,
145
145
  () => doPrefetch(props.href),
@@ -149,7 +149,7 @@ export function useLink(props: LinkProps): UseLinkReturn {
149
149
  const isActive = () => {
150
150
  const currentPath = router.currentRoute()?.path
151
151
  if (!currentPath || !props.href) return false
152
- if (props.href === '/') return currentPath === '/'
152
+ if (props.href === "/") return currentPath === "/"
153
153
  return currentPath.startsWith(props.href)
154
154
  }
155
155
 
@@ -163,9 +163,8 @@ export function useLink(props: LinkProps): UseLinkReturn {
163
163
  const cls: string[] = []
164
164
  if (props.class) cls.push(props.class)
165
165
  if (props.activeClass && isActive()) cls.push(props.activeClass)
166
- if (props.exactActiveClass && isExactActive())
167
- cls.push(props.exactActiveClass)
168
- return cls.join(' ')
166
+ if (props.exactActiveClass && isExactActive()) cls.push(props.exactActiveClass)
167
+ return cls.join(" ")
169
168
  }
170
169
 
171
170
  return {
@@ -191,8 +190,8 @@ export function useLink(props: LinkProps): UseLinkReturn {
191
190
  * <button
192
191
  * ref={props.ref}
193
192
  * class={props.class}
194
- * onclick={props.onClick}
195
- * onmouseenter={props.onMouseEnter}
193
+ * onClick={props.onClick}
194
+ * onMouseEnter={props.onMouseEnter}
196
195
  * >
197
196
  * {props.children}
198
197
  * </button>
@@ -203,8 +202,8 @@ export function useLink(props: LinkProps): UseLinkReturn {
203
202
  * <div
204
203
  * ref={props.ref}
205
204
  * class={`card ${props.isActive() ? "card--active" : ""}`}
206
- * onclick={props.onClick}
207
- * onmouseenter={props.onMouseEnter}
205
+ * onClick={props.onClick}
206
+ * onMouseEnter={props.onMouseEnter}
208
207
  * >
209
208
  * {props.children}
210
209
  * </div>
@@ -214,9 +213,7 @@ export function useLink(props: LinkProps): UseLinkReturn {
214
213
  * <ButtonLink href="/about">About</ButtonLink>
215
214
  * <CardLink href="/posts" prefetch="viewport">Posts</CardLink>
216
215
  */
217
- export function createLink(
218
- Component: (props: LinkRenderProps) => any,
219
- ): (props: LinkProps) => any {
216
+ export function createLink(Component: (props: LinkRenderProps) => any): (props: LinkProps) => any {
220
217
  return function WrappedLink(props: LinkProps) {
221
218
  const link = useLink(props)
222
219
 
@@ -231,8 +228,8 @@ export function createLink(
231
228
  isExactActive={link.isExactActive}
232
229
  class={link.classes}
233
230
  {...(props.style ? { style: props.style } : {})}
234
- {...(props.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
235
- {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}
231
+ {...(props.external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
232
+ {...(props["aria-label"] ? { "aria-label": props["aria-label"] } : {})}
236
233
  children={props.children}
237
234
  />
238
235
  )
@@ -248,17 +245,17 @@ export function createLink(
248
245
  */
249
246
  export const Link = createLink((props: LinkRenderProps) => (
250
247
  <a
251
- ref={props.ref as any}
248
+ ref={props.ref}
252
249
  href={props.href}
253
250
  {...(props.class ? { class: props.class } : {})}
254
251
  {...(props.style ? { style: props.style } : {})}
255
252
  {...(props.target ? { target: props.target } : {})}
256
253
  {...(props.rel ? { rel: props.rel } : {})}
257
- {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}
258
- {...(props.isExactActive() ? { 'aria-current': 'page' as const } : {})}
259
- onclick={props.onClick}
260
- onmouseenter={props.onMouseEnter}
261
- ontouchstart={props.onTouchStart}
254
+ {...(props["aria-label"] ? { "aria-label": props["aria-label"] } : {})}
255
+ {...(props.isExactActive() ? { "aria-current": "page" as const } : {})}
256
+ onClick={props.onClick}
257
+ onMouseEnter={props.onMouseEnter}
258
+ onTouchStart={props.onTouchStart}
262
259
  >
263
260
  {props.children}
264
261
  </a>
package/src/rate-limit.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Middleware, MiddlewareContext } from '@pyreon/server'
1
+ import type { Middleware, MiddlewareContext } from "@pyreon/server"
2
2
 
3
3
  // ─── Rate limiting middleware ───────────────────────────────────────────────
4
4
 
@@ -61,7 +61,7 @@ export function rateLimitMiddleware(config: RateLimitConfig = {}): Middleware {
61
61
  }, windowMs)
62
62
 
63
63
  // Allow GC to clean up the interval
64
- if (typeof cleanupInterval === 'object' && 'unref' in cleanupInterval) {
64
+ if (typeof cleanupInterval === "object" && "unref" in cleanupInterval) {
65
65
  cleanupInterval.unref()
66
66
  }
67
67
 
@@ -84,21 +84,21 @@ export function rateLimitMiddleware(config: RateLimitConfig = {}): Middleware {
84
84
  const resetSeconds = Math.ceil((entry.resetAt - now) / 1000)
85
85
 
86
86
  // Set rate limit headers on all responses
87
- ctx.headers.set('X-RateLimit-Limit', String(max))
88
- ctx.headers.set('X-RateLimit-Remaining', String(remaining))
89
- ctx.headers.set('X-RateLimit-Reset', String(resetSeconds))
87
+ ctx.headers.set("X-RateLimit-Limit", String(max))
88
+ ctx.headers.set("X-RateLimit-Remaining", String(remaining))
89
+ ctx.headers.set("X-RateLimit-Reset", String(resetSeconds))
90
90
 
91
91
  if (entry.count > max) {
92
92
  if (onLimit) return onLimit(ctx)
93
93
 
94
- return new Response(JSON.stringify({ error: 'Too many requests' }), {
94
+ return new Response(JSON.stringify({ error: "Too many requests" }), {
95
95
  status: 429,
96
96
  headers: {
97
- 'Content-Type': 'application/json',
98
- 'Retry-After': String(resetSeconds),
99
- 'X-RateLimit-Limit': String(max),
100
- 'X-RateLimit-Remaining': '0',
101
- 'X-RateLimit-Reset': String(resetSeconds),
97
+ "Content-Type": "application/json",
98
+ "Retry-After": String(resetSeconds),
99
+ "X-RateLimit-Limit": String(max),
100
+ "X-RateLimit-Remaining": "0",
101
+ "X-RateLimit-Reset": String(resetSeconds),
102
102
  },
103
103
  })
104
104
  }
@@ -107,15 +107,15 @@ export function rateLimitMiddleware(config: RateLimitConfig = {}): Middleware {
107
107
 
108
108
  function defaultKeyFn(ctx: MiddlewareContext): string {
109
109
  return (
110
- ctx.req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??
111
- ctx.req.headers.get('x-real-ip') ??
112
- 'unknown'
110
+ ctx.req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
111
+ ctx.req.headers.get("x-real-ip") ??
112
+ "unknown"
113
113
  )
114
114
  }
115
115
 
116
116
  /** Simple glob matching for path patterns. Supports trailing `*`. */
117
117
  function matchSimpleGlob(pattern: string, path: string): boolean {
118
- if (pattern.endsWith('/*')) {
118
+ if (pattern.endsWith("/*")) {
119
119
  return path.startsWith(pattern.slice(0, -1))
120
120
  }
121
121
  return pattern === path
package/src/script.tsx CHANGED
@@ -1,6 +1,6 @@
1
- import type { VNodeChild } from '@pyreon/core'
2
- import { createRef, onMount, onUnmount } from '@pyreon/core'
3
- import { useIntersectionObserver } from './utils/use-intersection-observer'
1
+ import type { VNodeChild } from "@pyreon/core"
2
+ import { createRef, onMount, onUnmount } from "@pyreon/core"
3
+ import { useIntersectionObserver } from "./utils/use-intersection-observer"
4
4
 
5
5
  // ─── Script optimization component ─────────────────────────────────────────
6
6
  //
@@ -29,11 +29,11 @@ export interface ScriptProps {
29
29
  }
30
30
 
31
31
  export type ScriptStrategy =
32
- | 'beforeHydration'
33
- | 'afterHydration'
34
- | 'onIdle'
35
- | 'onInteraction'
36
- | 'onViewport'
32
+ | "beforeHydration"
33
+ | "afterHydration"
34
+ | "onIdle"
35
+ | "onInteraction"
36
+ | "onViewport"
37
37
 
38
38
  /**
39
39
  * Optimized script loading component.
@@ -55,15 +55,14 @@ export function Script(props: ScriptProps): VNodeChild {
55
55
  // Deduplication
56
56
  if (props.id && document.getElementById(props.id)) return
57
57
 
58
- const script = document.createElement('script')
58
+ const script = document.createElement("script")
59
59
  if (props.src) script.src = props.src
60
60
  if (props.id) script.id = props.id
61
61
  script.async = props.async !== false
62
62
 
63
63
  if (props.onLoad) script.onload = props.onLoad
64
64
  if (props.onError) {
65
- script.onerror = () =>
66
- props.onError?.(new Error(`Failed to load: ${props.src}`))
65
+ script.onerror = () => props.onError?.(new Error(`Failed to load: ${props.src}`))
67
66
  }
68
67
 
69
68
  if (props.children && !props.src) {
@@ -74,28 +73,28 @@ export function Script(props: ScriptProps): VNodeChild {
74
73
  }
75
74
 
76
75
  onMount(() => {
77
- const strategy = props.strategy ?? 'afterHydration'
76
+ const strategy = props.strategy ?? "afterHydration"
78
77
 
79
78
  switch (strategy) {
80
- case 'beforeHydration':
79
+ case "beforeHydration":
81
80
  // Already in HTML — do nothing
82
81
  break
83
82
 
84
- case 'afterHydration':
83
+ case "afterHydration":
85
84
  // Load immediately after mount (hydration is complete)
86
85
  loadScript()
87
86
  break
88
87
 
89
- case 'onIdle':
90
- if ('requestIdleCallback' in window) {
88
+ case "onIdle":
89
+ if ("requestIdleCallback" in window) {
91
90
  requestIdleCallback(() => loadScript(), { timeout: 5000 })
92
91
  } else {
93
92
  setTimeout(loadScript, 200)
94
93
  }
95
94
  break
96
95
 
97
- case 'onInteraction': {
98
- const events = ['click', 'scroll', 'keydown', 'touchstart']
96
+ case "onInteraction": {
97
+ const events = ["click", "scroll", "keydown", "touchstart"]
99
98
  function handler() {
100
99
  for (const e of events) document.removeEventListener(e, handler)
101
100
  loadScript()
@@ -109,7 +108,7 @@ export function Script(props: ScriptProps): VNodeChild {
109
108
  break
110
109
  }
111
110
 
112
- case 'onViewport':
111
+ case "onViewport":
113
112
  // Handled below via useIntersectionObserver on the sentinel element
114
113
  break
115
114
  }
@@ -117,16 +116,16 @@ export function Script(props: ScriptProps): VNodeChild {
117
116
  })
118
117
 
119
118
  const sentinelRef = createRef<HTMLElement>()
120
- const strategy = props.strategy ?? 'afterHydration'
119
+ const strategy = props.strategy ?? "afterHydration"
121
120
 
122
- if (strategy === 'onViewport') {
121
+ if (strategy === "onViewport") {
123
122
  useIntersectionObserver(
124
123
  () => sentinelRef.current ?? undefined,
125
124
  () => loadScript(),
126
125
  )
127
126
  }
128
127
 
129
- if (strategy === 'onViewport') {
128
+ if (strategy === "onViewport") {
130
129
  return <div ref={sentinelRef} style="width:0;height:0;overflow:hidden" />
131
130
  }
132
131
 
package/src/seo.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Middleware } from '@pyreon/server'
2
- import type { Plugin } from 'vite'
1
+ import type { Middleware } from "@pyreon/server"
2
+ import type { Plugin } from "vite"
3
3
 
4
4
  // ─── SEO utilities ──────────────────────────────────────────────────────────
5
5
  //
@@ -29,47 +29,37 @@ export interface SitemapEntry {
29
29
  lastmod?: string
30
30
  }
31
31
 
32
- export type ChangeFreq =
33
- | 'always'
34
- | 'hourly'
35
- | 'daily'
36
- | 'weekly'
37
- | 'monthly'
38
- | 'yearly'
39
- | 'never'
32
+ export type ChangeFreq = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never"
40
33
 
41
34
  /**
42
35
  * Generate a sitemap.xml string from route file paths.
43
36
  */
44
- export function generateSitemap(
45
- routeFiles: string[],
46
- config: SitemapConfig,
47
- ): string {
48
- const { origin, exclude = [], changefreq = 'weekly', priority = 0.7 } = config
37
+ export function generateSitemap(routeFiles: string[], config: SitemapConfig): string {
38
+ const { origin, exclude = [], changefreq = "weekly", priority = 0.7 } = config
49
39
 
50
40
  const paths = routeFiles
51
41
  .filter((f) => {
52
42
  // Exclude layout, error, loading files
53
43
  const name = f
54
- .split('/')
44
+ .split("/")
55
45
  .pop()
56
- ?.replace(/\.\w+$/, '')
57
- return name !== '_layout' && name !== '_error' && name !== '_loading'
46
+ ?.replace(/\.\w+$/, "")
47
+ return name !== "_layout" && name !== "_error" && name !== "_loading"
58
48
  })
59
49
  .map((f) => {
60
50
  // Convert file path to URL
61
51
  let path = f
62
- .replace(/\.\w+$/, '')
63
- .replace(/\/index$/, '/')
64
- .replace(/^index$/, '/')
52
+ .replace(/\.\w+$/, "")
53
+ .replace(/\/index$/, "/")
54
+ .replace(/^index$/, "/")
65
55
 
66
56
  // Skip dynamic routes — they need additionalPaths
67
- if (path.includes('[')) return null
57
+ if (path.includes("[")) return null
68
58
 
69
59
  // Strip route groups
70
- path = path.replace(/\([\w-]+\)\//g, '')
60
+ path = path.replace(/\([\w-]+\)\//g, "")
71
61
 
72
- if (!path.startsWith('/')) path = `/${path}`
62
+ if (!path.startsWith("/")) path = `/${path}`
73
63
  return path
74
64
  })
75
65
  .filter((p): p is string => p !== null)
@@ -82,14 +72,14 @@ export function generateSitemap(
82
72
 
83
73
  const entries = allPaths
84
74
  .map((entry) => {
85
- const loc = `${origin}${entry.path === '/' ? '' : entry.path}`
75
+ const loc = `${origin}${entry.path === "/" ? "" : entry.path}`
86
76
  return ` <url>
87
77
  <loc>${escapeXml(loc)}</loc>
88
78
  <changefreq>${entry.changefreq ?? changefreq}</changefreq>
89
- <priority>${entry.priority ?? priority}</priority>${entry.lastmod ? `\n <lastmod>${entry.lastmod}</lastmod>` : ''}
79
+ <priority>${entry.priority ?? priority}</priority>${entry.lastmod ? `\n <lastmod>${entry.lastmod}</lastmod>` : ""}
90
80
  </url>`
91
81
  })
92
- .join('\n')
82
+ .join("\n")
93
83
 
94
84
  return `<?xml version="1.0" encoding="UTF-8"?>
95
85
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
@@ -99,11 +89,11 @@ ${entries}
99
89
 
100
90
  function escapeXml(str: string): string {
101
91
  return str
102
- .replace(/&/g, '&amp;')
103
- .replace(/</g, '&lt;')
104
- .replace(/>/g, '&gt;')
105
- .replace(/"/g, '&quot;')
106
- .replace(/'/g, '&apos;')
92
+ .replace(/&/g, "&amp;")
93
+ .replace(/</g, "&lt;")
94
+ .replace(/>/g, "&gt;")
95
+ .replace(/"/g, "&quot;")
96
+ .replace(/'/g, "&apos;")
107
97
  }
108
98
 
109
99
  // ─── Robots.txt ─────────────────────────────────────────────────────────────
@@ -128,7 +118,7 @@ export interface RobotsRule {
128
118
  * Generate a robots.txt string.
129
119
  */
130
120
  export function generateRobots(config: RobotsConfig = {}): string {
131
- const { rules = [{ userAgent: '*', allow: ['/'] }], sitemap, host } = config
121
+ const { rules = [{ userAgent: "*", allow: ["/"] }], sitemap, host } = config
132
122
  const lines: string[] = []
133
123
 
134
124
  for (const rule of rules) {
@@ -140,27 +130,27 @@ export function generateRobots(config: RobotsConfig = {}): string {
140
130
  for (const path of rule.disallow) lines.push(`Disallow: ${path}`)
141
131
  }
142
132
  if (rule.crawlDelay) lines.push(`Crawl-delay: ${rule.crawlDelay}`)
143
- lines.push('')
133
+ lines.push("")
144
134
  }
145
135
 
146
136
  if (sitemap) lines.push(`Sitemap: ${sitemap}`)
147
137
  if (host) lines.push(`Host: ${host}`)
148
138
 
149
- return lines.join('\n')
139
+ return lines.join("\n")
150
140
  }
151
141
 
152
142
  // ─── Structured data (JSON-LD) ──────────────────────────────────────────────
153
143
 
154
144
  export type JsonLdType =
155
- | 'WebSite'
156
- | 'WebPage'
157
- | 'Article'
158
- | 'BlogPosting'
159
- | 'Product'
160
- | 'Organization'
161
- | 'Person'
162
- | 'BreadcrumbList'
163
- | 'FAQPage'
145
+ | "WebSite"
146
+ | "WebPage"
147
+ | "Article"
148
+ | "BlogPosting"
149
+ | "Product"
150
+ | "Organization"
151
+ | "Person"
152
+ | "BreadcrumbList"
153
+ | "FAQPage"
164
154
  | (string & {})
165
155
 
166
156
  /**
@@ -177,7 +167,7 @@ export type JsonLdType =
177
167
  */
178
168
  export function jsonLd(data: Record<string, unknown>): string {
179
169
  const ld = {
180
- '@context': 'https://schema.org',
170
+ "@context": "https://schema.org",
181
171
  ...data,
182
172
  }
183
173
  return `<script type="application/ld+json">${JSON.stringify(ld)}</script>`
@@ -212,13 +202,13 @@ export interface SeoPluginConfig {
212
202
  */
213
203
  export function seoPlugin(config: SeoPluginConfig = {}): Plugin {
214
204
  return {
215
- name: 'pyreon-zero-seo',
216
- apply: 'build',
205
+ name: "pyreon-zero-seo",
206
+ apply: "build",
217
207
 
218
208
  async generateBundle(_, _bundle) {
219
209
  // Generate sitemap.xml
220
210
  if (config.sitemap) {
221
- const { scanRouteFiles } = await import('./fs-router')
211
+ const { scanRouteFiles } = await import("./fs-router")
222
212
  const routesDir = `${process.cwd()}/src/routes`
223
213
 
224
214
  try {
@@ -226,8 +216,8 @@ export function seoPlugin(config: SeoPluginConfig = {}): Plugin {
226
216
  const sitemap = generateSitemap(files, config.sitemap)
227
217
 
228
218
  this.emitFile({
229
- type: 'asset',
230
- fileName: 'sitemap.xml',
219
+ type: "asset",
220
+ fileName: "sitemap.xml",
231
221
  source: sitemap,
232
222
  })
233
223
  } catch {
@@ -240,8 +230,8 @@ export function seoPlugin(config: SeoPluginConfig = {}): Plugin {
240
230
  const robots = generateRobots(config.robots)
241
231
 
242
232
  this.emitFile({
243
- type: 'asset',
244
- fileName: 'robots.txt',
233
+ type: "asset",
234
+ fileName: "robots.txt",
245
235
  source: robots,
246
236
  })
247
237
  }
@@ -257,21 +247,21 @@ export function seoPlugin(config: SeoPluginConfig = {}): Plugin {
257
247
  */
258
248
  export function seoMiddleware(config: SeoPluginConfig = {}): Middleware {
259
249
  return async (ctx) => {
260
- if (ctx.url.pathname === '/robots.txt' && config.robots) {
250
+ if (ctx.url.pathname === "/robots.txt" && config.robots) {
261
251
  return new Response(generateRobots(config.robots), {
262
- headers: { 'Content-Type': 'text/plain' },
252
+ headers: { "Content-Type": "text/plain" },
263
253
  })
264
254
  }
265
255
 
266
- if (ctx.url.pathname === '/sitemap.xml' && config.sitemap) {
256
+ if (ctx.url.pathname === "/sitemap.xml" && config.sitemap) {
267
257
  try {
268
- const { scanRouteFiles } = await import('./fs-router')
258
+ const { scanRouteFiles } = await import("./fs-router")
269
259
  const routesDir = `${process.cwd()}/src/routes`
270
260
  const files = await scanRouteFiles(routesDir)
271
261
  const sitemap = generateSitemap(files, config.sitemap)
272
262
 
273
263
  return new Response(sitemap, {
274
- headers: { 'Content-Type': 'application/xml' },
264
+ headers: { "Content-Type": "application/xml" },
275
265
  })
276
266
  } catch {
277
267
  // Sitemap generation failed — continue to rendering
package/src/sharp.d.ts CHANGED
@@ -1,10 +1,6 @@
1
- declare module 'sharp' {
1
+ declare module "sharp" {
2
2
  interface SharpInstance {
3
- resize(
4
- width: number,
5
- height?: number,
6
- options?: { fit?: string },
7
- ): SharpInstance
3
+ resize(width: number, height?: number, options?: { fit?: string }): SharpInstance
8
4
  webp(options?: { quality?: number }): SharpInstance
9
5
  avif(options?: { quality?: number }): SharpInstance
10
6
  jpeg(options?: { quality?: number; mozjpeg?: boolean }): SharpInstance