@pyreon/zero 0.15.0 → 0.18.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.
- package/lib/{api-routes-DANluJic.js → api-routes-Ci0kVmM4.js} +2 -2
- package/lib/client.js +4 -1
- package/lib/env.js +6 -6
- package/lib/font.js +3 -3
- package/lib/{fs-router-ZebyutPa.js → fs-router-MewHc5SB.js} +25 -30
- package/lib/i18n-routing.js +112 -1
- package/lib/image.js +140 -58
- package/lib/index.js +252 -82
- package/lib/og-image.js +5 -5
- package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
- package/lib/script.js +114 -25
- package/lib/seo.js +186 -15
- package/lib/server.js +274 -564
- package/lib/types/config.d.ts +307 -3
- package/lib/types/env.d.ts +2 -2
- package/lib/types/i18n-routing.d.ts +193 -2
- package/lib/types/image.d.ts +105 -5
- package/lib/types/index.d.ts +666 -182
- package/lib/types/script.d.ts +78 -6
- package/lib/types/seo.d.ts +128 -4
- package/lib/types/server.d.ts +607 -72
- package/lib/vite-plugin-y0NmCLJA.js +2476 -0
- package/package.json +11 -10
- package/src/adapters/bun.ts +20 -1
- package/src/adapters/cloudflare.ts +78 -1
- package/src/adapters/index.ts +25 -3
- package/src/adapters/netlify.ts +63 -1
- package/src/adapters/node.ts +25 -1
- package/src/adapters/static.ts +26 -1
- package/src/adapters/validate.ts +8 -1
- package/src/adapters/vercel.ts +76 -1
- package/src/adapters/warn-missing-env.ts +49 -0
- package/src/app.ts +14 -0
- package/src/client.ts +18 -0
- package/src/entry-server.ts +55 -5
- package/src/env.ts +7 -7
- package/src/font.ts +3 -3
- package/src/fs-router.ts +72 -3
- package/src/i18n-routing.ts +246 -12
- package/src/image.tsx +242 -91
- package/src/index.ts +4 -4
- package/src/isr.ts +24 -6
- package/src/manifest.ts +675 -0
- package/src/og-image.ts +5 -5
- package/src/script.tsx +159 -36
- package/src/seo.ts +346 -15
- package/src/server.ts +10 -2
- package/src/ssg-plugin.ts +1211 -54
- package/src/types.ts +333 -10
- package/src/vercel-revalidate-handler.ts +204 -0
- package/src/vite-plugin.ts +171 -41
- package/lib/vite-plugin-E4BHYvYW.js +0 -855
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
|
|
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
|
-
|
|
161
|
+
w += fontSize * 1.0
|
|
162
162
|
} else if (code <= 0x7E && 'iljft!|:;.,\''.includes(s[i]!)) {
|
|
163
163
|
// Narrow Latin characters
|
|
164
|
-
|
|
164
|
+
w += fontSize * 0.35
|
|
165
165
|
} else {
|
|
166
166
|
// Regular Latin characters
|
|
167
|
-
|
|
167
|
+
w += fontSize * 0.55
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
-
return
|
|
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
|
-
//
|
|
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
|
-
*
|
|
75
|
+
* Composable that provides all script loading behavior — strategy state
|
|
76
|
+
* machine (afterHydration / onIdle / onInteraction / onViewport),
|
|
77
|
+
* deduplication, load/error tracking.
|
|
40
78
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
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
|
|
56
|
-
// SSR-safety contract at the callsite (the rule can't
|
|
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))
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
188
|
+
() => sentinelRef!.current ?? undefined,
|
|
128
189
|
() => loadScript(),
|
|
129
190
|
)
|
|
130
191
|
}
|
|
131
192
|
|
|
132
|
-
|
|
133
|
-
|
|
193
|
+
return {
|
|
194
|
+
sentinelRef,
|
|
195
|
+
loaded,
|
|
196
|
+
errored,
|
|
197
|
+
pending,
|
|
198
|
+
needsSentinel: strategy === 'onViewport',
|
|
199
|
+
load: loadScript,
|
|
134
200
|
}
|
|
201
|
+
}
|
|
135
202
|
|
|
136
|
-
|
|
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
|
+
})
|