@pyreon/zero 0.14.0 → 0.16.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 (114) hide show
  1. package/lib/api-routes-Ci0kVmM4.js +146 -0
  2. package/lib/client.js +7 -2
  3. package/lib/csp.js +19 -9
  4. package/lib/env.js +6 -6
  5. package/lib/font.js +3 -3
  6. package/lib/{fs-router-CQ7Zxeca.js → fs-router-MewHc5SB.js} +56 -24
  7. package/lib/i18n-routing.js +112 -1
  8. package/lib/image-plugin.js +4 -0
  9. package/lib/image.js +141 -108
  10. package/lib/index.js +253 -132
  11. package/lib/link.js +1 -49
  12. package/lib/og-image.js +5 -5
  13. package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
  14. package/lib/script.js +115 -74
  15. package/lib/seo.js +186 -15
  16. package/lib/server.js +275 -1247
  17. package/lib/theme.js +1 -50
  18. package/lib/types/config.d.ts +275 -3
  19. package/lib/types/env.d.ts +2 -2
  20. package/lib/types/i18n-routing.d.ts +197 -6
  21. package/lib/types/image.d.ts +105 -5
  22. package/lib/types/index.d.ts +640 -178
  23. package/lib/types/link.d.ts +3 -3
  24. package/lib/types/script.d.ts +78 -6
  25. package/lib/types/seo.d.ts +128 -4
  26. package/lib/types/server.d.ts +603 -77
  27. package/lib/types/theme.d.ts +2 -2
  28. package/lib/vite-plugin-xjWZwudX.js +2454 -0
  29. package/package.json +16 -13
  30. package/src/adapters/bun.ts +20 -1
  31. package/src/adapters/cloudflare.ts +78 -1
  32. package/src/adapters/index.ts +25 -3
  33. package/src/adapters/netlify.ts +63 -1
  34. package/src/adapters/node.ts +25 -1
  35. package/src/adapters/static.ts +26 -1
  36. package/src/adapters/validate.ts +8 -1
  37. package/src/adapters/vercel.ts +76 -1
  38. package/src/adapters/warn-missing-env.ts +49 -0
  39. package/src/app.ts +35 -1
  40. package/src/client.ts +18 -0
  41. package/src/csp.ts +28 -12
  42. package/src/entry-server.ts +55 -5
  43. package/src/env.ts +7 -7
  44. package/src/font.ts +3 -3
  45. package/src/fs-router.ts +123 -4
  46. package/src/i18n-routing.ts +246 -12
  47. package/src/image.tsx +242 -91
  48. package/src/index.ts +4 -4
  49. package/src/isr.ts +24 -6
  50. package/src/manifest.ts +675 -0
  51. package/src/og-image.ts +5 -5
  52. package/src/script.tsx +159 -36
  53. package/src/seo.ts +346 -15
  54. package/src/server.ts +10 -2
  55. package/src/ssg-plugin.ts +1523 -0
  56. package/src/types.ts +329 -19
  57. package/src/vercel-revalidate-handler.ts +204 -0
  58. package/src/vite-plugin.ts +326 -68
  59. package/lib/actions.js.map +0 -1
  60. package/lib/ai.js.map +0 -1
  61. package/lib/api-routes.js.map +0 -1
  62. package/lib/cache.js.map +0 -1
  63. package/lib/client.js.map +0 -1
  64. package/lib/compression.js.map +0 -1
  65. package/lib/config.js.map +0 -1
  66. package/lib/cors.js.map +0 -1
  67. package/lib/csp.js.map +0 -1
  68. package/lib/env.js.map +0 -1
  69. package/lib/favicon.js.map +0 -1
  70. package/lib/font.js.map +0 -1
  71. package/lib/fs-router-3xzp-4Wj.js.map +0 -1
  72. package/lib/fs-router-CQ7Zxeca.js.map +0 -1
  73. package/lib/i18n-routing.js.map +0 -1
  74. package/lib/image-plugin.js.map +0 -1
  75. package/lib/image.js.map +0 -1
  76. package/lib/index.js.map +0 -1
  77. package/lib/link.js.map +0 -1
  78. package/lib/logger.js.map +0 -1
  79. package/lib/meta.js.map +0 -1
  80. package/lib/middleware.js.map +0 -1
  81. package/lib/og-image.js.map +0 -1
  82. package/lib/rate-limit.js.map +0 -1
  83. package/lib/script.js.map +0 -1
  84. package/lib/seo.js.map +0 -1
  85. package/lib/server.js.map +0 -1
  86. package/lib/testing.js.map +0 -1
  87. package/lib/theme.js.map +0 -1
  88. package/lib/types/actions.d.ts.map +0 -1
  89. package/lib/types/ai.d.ts.map +0 -1
  90. package/lib/types/api-routes.d.ts.map +0 -1
  91. package/lib/types/cache.d.ts.map +0 -1
  92. package/lib/types/client.d.ts.map +0 -1
  93. package/lib/types/compression.d.ts.map +0 -1
  94. package/lib/types/config.d.ts.map +0 -1
  95. package/lib/types/cors.d.ts.map +0 -1
  96. package/lib/types/csp.d.ts.map +0 -1
  97. package/lib/types/env.d.ts.map +0 -1
  98. package/lib/types/favicon.d.ts.map +0 -1
  99. package/lib/types/font.d.ts.map +0 -1
  100. package/lib/types/i18n-routing.d.ts.map +0 -1
  101. package/lib/types/image-plugin.d.ts.map +0 -1
  102. package/lib/types/image.d.ts.map +0 -1
  103. package/lib/types/index.d.ts.map +0 -1
  104. package/lib/types/link.d.ts.map +0 -1
  105. package/lib/types/logger.d.ts.map +0 -1
  106. package/lib/types/meta.d.ts.map +0 -1
  107. package/lib/types/middleware.d.ts.map +0 -1
  108. package/lib/types/og-image.d.ts.map +0 -1
  109. package/lib/types/rate-limit.d.ts.map +0 -1
  110. package/lib/types/script.d.ts.map +0 -1
  111. package/lib/types/seo.d.ts.map +0 -1
  112. package/lib/types/server.d.ts.map +0 -1
  113. package/lib/types/testing.d.ts.map +0 -1
  114. package/lib/types/theme.d.ts.map +0 -1
package/src/og-image.ts CHANGED
@@ -153,21 +153,21 @@ export function buildTextOverlaySvg(
153
153
  let currentLine = ''
154
154
 
155
155
  const estimateWidth = (s: string): number => {
156
- let width = 0
156
+ let w = 0
157
157
  for (let i = 0; i < s.length; i++) {
158
158
  const code = s.charCodeAt(i)
159
159
  if (code >= 0x3000 && code <= 0x9FFF) {
160
160
  // CJK characters — full width
161
- width += fontSize * 1.0
161
+ w += fontSize * 1.0
162
162
  } else if (code <= 0x7E && 'iljft!|:;.,\''.includes(s[i]!)) {
163
163
  // Narrow Latin characters
164
- width += fontSize * 0.35
164
+ w += fontSize * 0.35
165
165
  } else {
166
166
  // Regular Latin characters
167
- width += fontSize * 0.55
167
+ w += fontSize * 0.55
168
168
  }
169
169
  }
170
- return width
170
+ return w
171
171
  }
172
172
 
173
173
  for (const word of words) {
package/src/script.tsx CHANGED
@@ -1,5 +1,6 @@
1
- import type { VNodeChild } from '@pyreon/core'
1
+ import type { Ref, VNodeChild } from '@pyreon/core'
2
2
  import { createRef, onMount, onUnmount } from '@pyreon/core'
3
+ import { signal } from '@pyreon/reactivity'
3
4
  import { useIntersectionObserver } from './utils/use-intersection-observer'
4
5
 
5
6
  // ─── Script optimization component ─────────────────────────────────────────
@@ -9,7 +10,12 @@ import { useIntersectionObserver } from './utils/use-intersection-observer'
9
10
  // - Load on idle (requestIdleCallback)
10
11
  // - Load on interaction (click, scroll, etc.)
11
12
  // - Load on viewport entry
12
- // - Worker offloading for analytics scripts
13
+ //
14
+ // Three levels of API (mirrors @pyreon/zero/link and @pyreon/zero/image):
15
+ //
16
+ // 1. useScript(props) — composable returning load-state signals + sentinel ref
17
+ // 2. createScript(Comp) — HOC wrapping any component with script load behavior
18
+ // 3. Script — default sentinel-or-null component (built on createScript)
13
19
 
14
20
  export interface ScriptProps {
15
21
  /** Script source URL. */
@@ -22,9 +28,9 @@ export interface ScriptProps {
22
28
  id?: string
23
29
  /** Async attribute. Default: true */
24
30
  async?: boolean
25
- /** onLoad callback. */
31
+ /** onLoad callback — fires when the `<script>` finishes loading. */
26
32
  onLoad?: () => void
27
- /** onError callback. */
33
+ /** onError callback — fires when the `<script>` fails to load. */
28
34
  onError?: (error: Error) => void
29
35
  }
30
36
 
@@ -35,57 +41,115 @@ export type ScriptStrategy =
35
41
  | 'onInteraction'
36
42
  | 'onViewport'
37
43
 
44
+ /** Return type of {@link useScript}. */
45
+ export interface UseScriptReturn {
46
+ /** Ref — attach to the sentinel element for `onViewport` strategy. Undefined for other strategies. */
47
+ sentinelRef: Ref<HTMLElement> | undefined
48
+ /** Whether the script has finished loading (onLoad fired). */
49
+ loaded: () => boolean
50
+ /** Whether the script load failed (onError fired). */
51
+ errored: () => boolean
52
+ /** Whether the script is in the strategy state machine awaiting a trigger (idle/interaction/viewport). */
53
+ pending: () => boolean
54
+ /** Whether the consumer needs to render a sentinel element (only true for `onViewport`). */
55
+ needsSentinel: boolean
56
+ /** Imperatively trigger the script load. Already invoked automatically by the strategy. */
57
+ load: () => void
58
+ }
59
+
60
+ /** Props passed to a custom component via {@link createScript}. */
61
+ export interface ScriptRenderProps {
62
+ /** Ref — attach to whatever sentinel element you render (only matters for `onViewport`). */
63
+ sentinelRef: Ref<HTMLElement> | undefined
64
+ /** Whether the script is in viewport-wait mode (true → render a sentinel; false → render null). */
65
+ needsSentinel: boolean
66
+ /** Whether the script has finished loading (onLoad fired). */
67
+ loaded: () => boolean
68
+ /** Whether the script load failed (onError fired). */
69
+ errored: () => boolean
70
+ /** Whether the script is in the strategy state machine awaiting a trigger. */
71
+ pending: () => boolean
72
+ }
73
+
38
74
  /**
39
- * Optimized script loading component.
75
+ * Composable that provides all script loading behavior — strategy state
76
+ * machine (afterHydration / onIdle / onInteraction / onViewport),
77
+ * deduplication, load/error tracking.
40
78
  *
41
- * @example
42
- * // Load analytics after page is interactive
43
- * <Script src="https://analytics.example.com/script.js" strategy="onIdle" />
44
- *
45
- * // Load chat widget when user scrolls
46
- * <Script src="/chat-widget.js" strategy="onViewport" />
79
+ * Returns reactive signals (`loaded`, `errored`, `pending`) so consumers
80
+ * can render loading indicators, retry buttons, or analytics-readiness
81
+ * gates without re-implementing the strategy machine.
47
82
  *
48
- * // Inline script with deferred execution
49
- * <Script strategy="afterHydration">
50
- * {`console.log("App hydrated!")`}
51
- * </Script>
83
+ * @example
84
+ * function MyScript(props: ScriptProps) {
85
+ * const s = useScript(props)
86
+ * return (
87
+ * <>
88
+ * {() => s.loaded() ? <Analytics /> : <Skeleton />}
89
+ * {() => s.needsSentinel && <div ref={s.sentinelRef} style="width:0;height:0" />}
90
+ * </>
91
+ * )
92
+ * }
52
93
  */
53
- export function Script(props: ScriptProps): VNodeChild {
94
+ export function useScript(props: ScriptProps): UseScriptReturn {
95
+ const strategy = props.strategy ?? 'afterHydration'
96
+ const loaded = signal(false)
97
+ const errored = signal(false)
98
+ const pending = signal(strategy !== 'beforeHydration' && strategy !== 'afterHydration')
99
+ const sentinelRef = strategy === 'onViewport' ? createRef<HTMLElement>() : undefined
100
+
54
101
  function loadScript() {
55
- // Only invoked from `onMount` — explicit guard documents the
56
- // SSR-safety contract at the callsite (the rule can't AST-trace the
57
- // indirect call).
102
+ // Only invoked from `onMount` or strategy triggers — explicit guard
103
+ // documents the SSR-safety contract at the callsite (the rule can't
104
+ // AST-trace the indirect call).
58
105
  if (typeof document === 'undefined') return
59
- // Deduplication
60
- if (props.id && document.getElementById(props.id)) return
106
+ // Deduplication — short-circuit if a script with the same id exists.
107
+ if (props.id && document.getElementById(props.id)) {
108
+ loaded.set(true)
109
+ pending.set(false)
110
+ return
111
+ }
61
112
 
62
113
  const script = document.createElement('script')
63
114
  if (props.src) script.src = props.src
64
115
  if (props.id) script.id = props.id
65
116
  script.async = props.async !== false
66
117
 
67
- if (props.onLoad) script.onload = props.onLoad
68
- if (props.onError) {
69
- script.onerror = () => props.onError?.(new Error(`Failed to load: ${props.src}`))
118
+ script.onload = () => {
119
+ loaded.set(true)
120
+ pending.set(false)
121
+ props.onLoad?.()
122
+ }
123
+ script.onerror = () => {
124
+ errored.set(true)
125
+ pending.set(false)
126
+ props.onError?.(new Error(`Failed to load: ${props.src}`))
70
127
  }
71
128
 
72
129
  if (props.children && !props.src) {
73
130
  script.textContent = props.children
131
+ // Inline scripts have no async load event — mark loaded synchronously
132
+ // post-append so consumers can react. setTimeout 0 keeps the order
133
+ // (DOM append → script body executes → next microtask → signals update).
134
+ setTimeout(() => {
135
+ loaded.set(true)
136
+ pending.set(false)
137
+ }, 0)
74
138
  }
75
139
 
76
140
  document.head.appendChild(script)
77
141
  }
78
142
 
79
143
  onMount(() => {
80
- const strategy = props.strategy ?? 'afterHydration'
81
-
82
144
  switch (strategy) {
83
145
  case 'beforeHydration':
84
- // Already in HTML — do nothing
146
+ // Already in HTML — do nothing.
147
+ loaded.set(true)
148
+ pending.set(false)
85
149
  break
86
150
 
87
151
  case 'afterHydration':
88
- // Load immediately after mount (hydration is complete)
152
+ // Load immediately after mount (hydration is complete).
89
153
  loadScript()
90
154
  break
91
155
 
@@ -113,25 +177,84 @@ export function Script(props: ScriptProps): VNodeChild {
113
177
  }
114
178
 
115
179
  case 'onViewport':
116
- // Handled below via useIntersectionObserver on the sentinel element
180
+ // Handled below via useIntersectionObserver on the sentinel ref.
117
181
  break
118
182
  }
119
183
  return undefined
120
184
  })
121
185
 
122
- const sentinelRef = createRef<HTMLElement>()
123
- const strategy = props.strategy ?? 'afterHydration'
124
-
125
186
  if (strategy === 'onViewport') {
126
187
  useIntersectionObserver(
127
- () => sentinelRef.current ?? undefined,
188
+ () => sentinelRef!.current ?? undefined,
128
189
  () => loadScript(),
129
190
  )
130
191
  }
131
192
 
132
- if (strategy === 'onViewport') {
133
- return <div ref={sentinelRef} style="width:0;height:0;overflow:hidden" />
193
+ return {
194
+ sentinelRef,
195
+ loaded,
196
+ errored,
197
+ pending,
198
+ needsSentinel: strategy === 'onViewport',
199
+ load: loadScript,
134
200
  }
201
+ }
135
202
 
136
- return null
203
+ /**
204
+ * Higher-order component that wraps any component with script load behavior.
205
+ *
206
+ * The wrapped component receives {@link ScriptRenderProps} with the sentinel
207
+ * ref, load-state signals, and a `needsSentinel` flag. Use this when you want
208
+ * to render a loading indicator, retry button, or custom analytics-readiness
209
+ * gate around the script load.
210
+ *
211
+ * @example
212
+ * // Script with a loading indicator
213
+ * const TrackedScript = createScript((props) => (
214
+ * <>
215
+ * {() => props.pending() && <Spinner />}
216
+ * {() => props.errored() && <button onClick={() => location.reload()}>Retry</button>}
217
+ * {props.needsSentinel && <div ref={props.sentinelRef} style="width:0;height:0" />}
218
+ * </>
219
+ * ))
220
+ *
221
+ * <TrackedScript src="/analytics.js" strategy="onIdle" />
222
+ */
223
+ export function createScript(
224
+ Component: (p: ScriptRenderProps) => any,
225
+ ): (props: ScriptProps) => any {
226
+ return function WrappedScript(props: ScriptProps) {
227
+ const s = useScript(props)
228
+ return (
229
+ <Component
230
+ sentinelRef={s.sentinelRef}
231
+ needsSentinel={s.needsSentinel}
232
+ loaded={s.loaded}
233
+ errored={s.errored}
234
+ pending={s.pending}
235
+ />
236
+ )
237
+ }
137
238
  }
239
+
240
+ /**
241
+ * Default optimized script component. Renders a 0×0 sentinel `<div>` for the
242
+ * `onViewport` strategy (so IntersectionObserver has an element to observe),
243
+ * `null` for every other strategy.
244
+ *
245
+ * @example
246
+ * // Load analytics after page is interactive
247
+ * <Script src="https://analytics.example.com/script.js" strategy="onIdle" />
248
+ *
249
+ * // Load chat widget when user scrolls
250
+ * <Script src="/chat-widget.js" strategy="onViewport" />
251
+ *
252
+ * // Inline script with deferred execution
253
+ * <Script strategy="afterHydration">
254
+ * {`console.log("App hydrated!")`}
255
+ * </Script>
256
+ */
257
+ export const Script: (props: ScriptProps) => VNodeChild = createScript((props) => {
258
+ if (!props.needsSentinel) return null
259
+ return <div ref={props.sentinelRef} style="width:0;height:0;overflow:hidden" />
260
+ })