@pyreon/zero 0.15.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.
- 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 +275 -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 +634 -182
- package/lib/types/script.d.ts +78 -6
- package/lib/types/seo.d.ts +128 -4
- package/lib/types/server.d.ts +575 -72
- package/lib/vite-plugin-xjWZwudX.js +2454 -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 +301 -10
- package/src/vercel-revalidate-handler.ts +204 -0
- package/src/vite-plugin.ts +108 -30
- package/lib/vite-plugin-E4BHYvYW.js +0 -855
package/lib/index.js
CHANGED
|
@@ -31,30 +31,34 @@ function useIntersectionObserver(getElement, onIntersect, rootMargin = "200px")
|
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/image.tsx
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Composable that provides all image optimization behavior — lazy loading,
|
|
35
|
+
* srcset/sizes resolution, format selection, blur-placeholder state,
|
|
36
|
+
* load tracking.
|
|
36
37
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* <Image {...hero} alt="Hero" priority />
|
|
38
|
+
* Use this for full control when `createImage` is too opinionated about
|
|
39
|
+
* the surrounding markup (e.g. custom container layouts, non-`<div>`
|
|
40
|
+
* wrappers, additional overlay elements).
|
|
41
41
|
*
|
|
42
42
|
* @example
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* function MyImage(props: ImageProps) {
|
|
44
|
+
* const img = useImage(props)
|
|
45
|
+
* return (
|
|
46
|
+
* <figure ref={img.containerRef} style={img.containerStyle}>
|
|
47
|
+
* <img
|
|
48
|
+
* src={img.src}
|
|
49
|
+
* srcSet={img.srcSet}
|
|
50
|
+
* sizes={img.sizes}
|
|
51
|
+
* alt={props.alt}
|
|
52
|
+
* loading={img.loading}
|
|
53
|
+
* onLoad={img.handleLoad}
|
|
54
|
+
* style={img.imageStyle}
|
|
55
|
+
* />
|
|
56
|
+
* <figcaption>{props.alt}</figcaption>
|
|
57
|
+
* </figure>
|
|
58
|
+
* )
|
|
59
|
+
* }
|
|
45
60
|
*/
|
|
46
|
-
function
|
|
47
|
-
if (props.raw) return /* @__PURE__ */ jsx("img", {
|
|
48
|
-
src: props.src,
|
|
49
|
-
alt: props.alt,
|
|
50
|
-
width: props.width,
|
|
51
|
-
height: props.height,
|
|
52
|
-
class: props.class,
|
|
53
|
-
style: props.style,
|
|
54
|
-
decoding: props.decoding ?? "async",
|
|
55
|
-
loading: props.loading ?? "lazy",
|
|
56
|
-
fetchPriority: props.priority ? "high" : void 0
|
|
57
|
-
});
|
|
61
|
+
function useImage(props) {
|
|
58
62
|
const isEager = props.priority || props.loading === "eager";
|
|
59
63
|
const loaded = signal(isEager);
|
|
60
64
|
const inView = signal(isEager);
|
|
@@ -62,7 +66,7 @@ function Image(props) {
|
|
|
62
66
|
const resolvedSrcset = typeof props.srcset === "string" ? props.srcset : props.srcset?.map((s) => `${s.src} ${s.width}w`).join(", ");
|
|
63
67
|
const sizes = props.sizes ?? "100vw";
|
|
64
68
|
const fit = props.fit ?? "cover";
|
|
65
|
-
const hasFormats = props.formats && props.formats.length > 0;
|
|
69
|
+
const hasFormats = !!(props.formats && props.formats.length > 0);
|
|
66
70
|
const aspectRatio = `${props.width} / ${props.height}`;
|
|
67
71
|
if (!isEager) useIntersectionObserver(() => containerRef.current ?? void 0, () => inView.set(true));
|
|
68
72
|
const containerStyle = [
|
|
@@ -73,53 +77,131 @@ function Image(props) {
|
|
|
73
77
|
"width: 100%",
|
|
74
78
|
props.style
|
|
75
79
|
].filter(Boolean).join("; ");
|
|
76
|
-
const
|
|
80
|
+
const imageStyle = () => [
|
|
81
|
+
"display: block",
|
|
82
|
+
"width: 100%",
|
|
83
|
+
"height: 100%",
|
|
84
|
+
`object-fit: ${fit}`,
|
|
85
|
+
"transition: opacity 0.3s ease",
|
|
86
|
+
props.placeholder && !loaded() ? "opacity: 0" : "opacity: 1"
|
|
87
|
+
].join("; ");
|
|
88
|
+
const placeholderStyle = () => [
|
|
89
|
+
"position: absolute",
|
|
90
|
+
"inset: 0",
|
|
91
|
+
"width: 100%",
|
|
92
|
+
"height: 100%",
|
|
93
|
+
"object-fit: cover",
|
|
94
|
+
"filter: blur(20px)",
|
|
95
|
+
"transform: scale(1.1)",
|
|
96
|
+
"transition: opacity 0.4s ease",
|
|
97
|
+
loaded() ? "opacity: 0; pointer-events: none" : "opacity: 1"
|
|
98
|
+
].join("; ");
|
|
99
|
+
return {
|
|
100
|
+
containerRef,
|
|
101
|
+
inView,
|
|
102
|
+
loaded,
|
|
77
103
|
src: () => inView() ? props.src : "",
|
|
78
104
|
srcSet: () => !hasFormats && inView() && resolvedSrcset ? resolvedSrcset : "",
|
|
79
105
|
sizes: resolvedSrcset ? sizes : void 0,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
106
|
+
aspectRatio,
|
|
107
|
+
containerStyle,
|
|
108
|
+
imageStyle,
|
|
109
|
+
placeholderStyle,
|
|
83
110
|
loading: isEager ? "eager" : "lazy",
|
|
84
|
-
decoding: props.decoding ?? "async",
|
|
85
111
|
fetchPriority: props.priority ? "high" : void 0,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
handleLoad: () => loaded.set(true),
|
|
113
|
+
formats: props.formats,
|
|
114
|
+
hasFormats
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Higher-order component that wraps any component with image optimization.
|
|
119
|
+
*
|
|
120
|
+
* The wrapped component receives {@link ImageRenderProps} with the pre-rendered
|
|
121
|
+
* `image` JSX (bare `<img>` OR `<picture>` tree depending on formats), the
|
|
122
|
+
* pre-rendered `placeholder` JSX, and the container ref + styles. Consumers
|
|
123
|
+
* compose those pieces with whatever wrapper element / layout they want.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* // Custom figure-based image with caption
|
|
127
|
+
* const FigureImage = createImage((props) => (
|
|
128
|
+
* <figure ref={props.containerRef} class={props.class} style={props.containerStyle}>
|
|
129
|
+
* {props.placeholder}
|
|
130
|
+
* {props.image}
|
|
131
|
+
* <figcaption>Caption goes here</figcaption>
|
|
132
|
+
* </figure>
|
|
133
|
+
* ))
|
|
134
|
+
*
|
|
135
|
+
* // Usage — identical to default <Image>
|
|
136
|
+
* <FigureImage src="/hero.jpg" alt="Hero" width={1200} height={630} />
|
|
137
|
+
*/
|
|
138
|
+
function createImage(Component) {
|
|
139
|
+
return function WrappedImage(props) {
|
|
140
|
+
if (props.raw) return /* @__PURE__ */ jsx("img", {
|
|
141
|
+
src: props.src,
|
|
142
|
+
alt: props.alt,
|
|
143
|
+
width: props.width,
|
|
144
|
+
height: props.height,
|
|
145
|
+
class: props.class,
|
|
146
|
+
style: props.style,
|
|
147
|
+
decoding: props.decoding ?? "async",
|
|
148
|
+
loading: props.loading ?? "lazy",
|
|
149
|
+
fetchPriority: props.priority ? "high" : void 0
|
|
150
|
+
});
|
|
151
|
+
const img = useImage(props);
|
|
152
|
+
const imgEl = /* @__PURE__ */ jsx("img", {
|
|
153
|
+
src: img.src,
|
|
154
|
+
srcSet: img.srcSet,
|
|
155
|
+
sizes: img.sizes,
|
|
156
|
+
alt: props.alt,
|
|
157
|
+
width: props.width,
|
|
158
|
+
height: props.height,
|
|
159
|
+
loading: img.loading,
|
|
160
|
+
decoding: props.decoding ?? "async",
|
|
161
|
+
fetchPriority: img.fetchPriority,
|
|
162
|
+
onLoad: img.handleLoad,
|
|
163
|
+
style: img.imageStyle
|
|
164
|
+
});
|
|
165
|
+
const placeholderEl = props.placeholder ? /* @__PURE__ */ jsx("img", {
|
|
101
166
|
src: props.placeholder,
|
|
102
167
|
alt: "",
|
|
103
168
|
"aria-hidden": "true",
|
|
104
169
|
loading: "eager",
|
|
105
|
-
style:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"width: 100%",
|
|
109
|
-
"height: 100%",
|
|
110
|
-
"object-fit: cover",
|
|
111
|
-
"filter: blur(20px)",
|
|
112
|
-
"transform: scale(1.1)",
|
|
113
|
-
"transition: opacity 0.4s ease",
|
|
114
|
-
loaded() ? "opacity: 0; pointer-events: none" : "opacity: 1"
|
|
115
|
-
].join("; ")
|
|
116
|
-
}), hasFormats ? /* @__PURE__ */ jsxs("picture", { children: [props.formats?.map((fmt) => /* @__PURE__ */ jsx("source", {
|
|
170
|
+
style: img.placeholderStyle
|
|
171
|
+
}) : null;
|
|
172
|
+
const imageEl = img.hasFormats ? /* @__PURE__ */ jsxs("picture", { children: [img.formats?.map((fmt) => /* @__PURE__ */ jsx("source", {
|
|
117
173
|
type: fmt.type,
|
|
118
|
-
srcSet: () => inView() ? fmt.srcset ?? "" : "",
|
|
119
|
-
sizes
|
|
120
|
-
})), imgEl] }) : imgEl
|
|
121
|
-
|
|
174
|
+
srcSet: () => img.inView() ? fmt.srcset ?? "" : "",
|
|
175
|
+
sizes: img.sizes
|
|
176
|
+
})), imgEl] }) : imgEl;
|
|
177
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
178
|
+
containerRef: img.containerRef,
|
|
179
|
+
class: props.class,
|
|
180
|
+
containerStyle: img.containerStyle,
|
|
181
|
+
placeholder: placeholderEl,
|
|
182
|
+
image: imageEl
|
|
183
|
+
});
|
|
184
|
+
};
|
|
122
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Default optimized image component with lazy loading, responsive srcset,
|
|
188
|
+
* `<picture>` multi-format support, and blur-up placeholders.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* // With imagePlugin — spread the import directly
|
|
192
|
+
* import hero from "./hero.jpg?optimize"
|
|
193
|
+
* <Image {...hero} alt="Hero" priority />
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* // Manual usage
|
|
197
|
+
* <Image src="/hero.jpg" alt="Hero" width={1200} height={630} />
|
|
198
|
+
*/
|
|
199
|
+
const Image = createImage((props) => /* @__PURE__ */ jsxs("div", {
|
|
200
|
+
ref: props.containerRef,
|
|
201
|
+
class: props.class,
|
|
202
|
+
style: props.containerStyle,
|
|
203
|
+
children: [props.placeholder, props.image]
|
|
204
|
+
}));
|
|
123
205
|
|
|
124
206
|
//#endregion
|
|
125
207
|
//#region src/link.tsx
|
|
@@ -306,36 +388,67 @@ const Link = createLink((props) => /* @__PURE__ */ jsx("a", {
|
|
|
306
388
|
//#endregion
|
|
307
389
|
//#region src/script.tsx
|
|
308
390
|
/**
|
|
309
|
-
*
|
|
391
|
+
* Composable that provides all script loading behavior — strategy state
|
|
392
|
+
* machine (afterHydration / onIdle / onInteraction / onViewport),
|
|
393
|
+
* deduplication, load/error tracking.
|
|
310
394
|
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
395
|
+
* Returns reactive signals (`loaded`, `errored`, `pending`) so consumers
|
|
396
|
+
* can render loading indicators, retry buttons, or analytics-readiness
|
|
397
|
+
* gates without re-implementing the strategy machine.
|
|
314
398
|
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
399
|
+
* @example
|
|
400
|
+
* function MyScript(props: ScriptProps) {
|
|
401
|
+
* const s = useScript(props)
|
|
402
|
+
* return (
|
|
403
|
+
* <>
|
|
404
|
+
* {() => s.loaded() ? <Analytics /> : <Skeleton />}
|
|
405
|
+
* {() => s.needsSentinel && <div ref={s.sentinelRef} style="width:0;height:0" />}
|
|
406
|
+
* </>
|
|
407
|
+
* )
|
|
408
|
+
* }
|
|
322
409
|
*/
|
|
323
|
-
function
|
|
410
|
+
function useScript(props) {
|
|
411
|
+
const strategy = props.strategy ?? "afterHydration";
|
|
412
|
+
const loaded = signal(false);
|
|
413
|
+
const errored = signal(false);
|
|
414
|
+
const pending = signal(strategy !== "beforeHydration" && strategy !== "afterHydration");
|
|
415
|
+
const sentinelRef = strategy === "onViewport" ? createRef() : void 0;
|
|
324
416
|
function loadScript() {
|
|
325
417
|
if (typeof document === "undefined") return;
|
|
326
|
-
if (props.id && document.getElementById(props.id))
|
|
418
|
+
if (props.id && document.getElementById(props.id)) {
|
|
419
|
+
loaded.set(true);
|
|
420
|
+
pending.set(false);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
327
423
|
const script = document.createElement("script");
|
|
328
424
|
if (props.src) script.src = props.src;
|
|
329
425
|
if (props.id) script.id = props.id;
|
|
330
426
|
script.async = props.async !== false;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
427
|
+
script.onload = () => {
|
|
428
|
+
loaded.set(true);
|
|
429
|
+
pending.set(false);
|
|
430
|
+
props.onLoad?.();
|
|
431
|
+
};
|
|
432
|
+
script.onerror = () => {
|
|
433
|
+
errored.set(true);
|
|
434
|
+
pending.set(false);
|
|
435
|
+
props.onError?.(/* @__PURE__ */ new Error(`Failed to load: ${props.src}`));
|
|
436
|
+
};
|
|
437
|
+
if (props.children && !props.src) {
|
|
438
|
+
script.textContent = props.children;
|
|
439
|
+
setTimeout(() => {
|
|
440
|
+
loaded.set(true);
|
|
441
|
+
pending.set(false);
|
|
442
|
+
}, 0);
|
|
443
|
+
}
|
|
334
444
|
document.head.appendChild(script);
|
|
335
445
|
}
|
|
336
446
|
onMount(() => {
|
|
337
|
-
switch (
|
|
338
|
-
case "beforeHydration":
|
|
447
|
+
switch (strategy) {
|
|
448
|
+
case "beforeHydration":
|
|
449
|
+
loaded.set(true);
|
|
450
|
+
pending.set(false);
|
|
451
|
+
break;
|
|
339
452
|
case "afterHydration":
|
|
340
453
|
loadScript();
|
|
341
454
|
break;
|
|
@@ -366,15 +479,72 @@ function Script(props) {
|
|
|
366
479
|
case "onViewport": break;
|
|
367
480
|
}
|
|
368
481
|
});
|
|
369
|
-
const sentinelRef = createRef();
|
|
370
|
-
const strategy = props.strategy ?? "afterHydration";
|
|
371
482
|
if (strategy === "onViewport") useIntersectionObserver(() => sentinelRef.current ?? void 0, () => loadScript());
|
|
372
|
-
|
|
373
|
-
|
|
483
|
+
return {
|
|
484
|
+
sentinelRef,
|
|
485
|
+
loaded,
|
|
486
|
+
errored,
|
|
487
|
+
pending,
|
|
488
|
+
needsSentinel: strategy === "onViewport",
|
|
489
|
+
load: loadScript
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Higher-order component that wraps any component with script load behavior.
|
|
494
|
+
*
|
|
495
|
+
* The wrapped component receives {@link ScriptRenderProps} with the sentinel
|
|
496
|
+
* ref, load-state signals, and a `needsSentinel` flag. Use this when you want
|
|
497
|
+
* to render a loading indicator, retry button, or custom analytics-readiness
|
|
498
|
+
* gate around the script load.
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* // Script with a loading indicator
|
|
502
|
+
* const TrackedScript = createScript((props) => (
|
|
503
|
+
* <>
|
|
504
|
+
* {() => props.pending() && <Spinner />}
|
|
505
|
+
* {() => props.errored() && <button onClick={() => location.reload()}>Retry</button>}
|
|
506
|
+
* {props.needsSentinel && <div ref={props.sentinelRef} style="width:0;height:0" />}
|
|
507
|
+
* </>
|
|
508
|
+
* ))
|
|
509
|
+
*
|
|
510
|
+
* <TrackedScript src="/analytics.js" strategy="onIdle" />
|
|
511
|
+
*/
|
|
512
|
+
function createScript(Component) {
|
|
513
|
+
return function WrappedScript(props) {
|
|
514
|
+
const s = useScript(props);
|
|
515
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
516
|
+
sentinelRef: s.sentinelRef,
|
|
517
|
+
needsSentinel: s.needsSentinel,
|
|
518
|
+
loaded: s.loaded,
|
|
519
|
+
errored: s.errored,
|
|
520
|
+
pending: s.pending
|
|
521
|
+
});
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Default optimized script component. Renders a 0×0 sentinel `<div>` for the
|
|
526
|
+
* `onViewport` strategy (so IntersectionObserver has an element to observe),
|
|
527
|
+
* `null` for every other strategy.
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* // Load analytics after page is interactive
|
|
531
|
+
* <Script src="https://analytics.example.com/script.js" strategy="onIdle" />
|
|
532
|
+
*
|
|
533
|
+
* // Load chat widget when user scrolls
|
|
534
|
+
* <Script src="/chat-widget.js" strategy="onViewport" />
|
|
535
|
+
*
|
|
536
|
+
* // Inline script with deferred execution
|
|
537
|
+
* <Script strategy="afterHydration">
|
|
538
|
+
* {`console.log("App hydrated!")`}
|
|
539
|
+
* <\/Script>
|
|
540
|
+
*/
|
|
541
|
+
const Script = createScript((props) => {
|
|
542
|
+
if (!props.needsSentinel) return null;
|
|
543
|
+
return /* @__PURE__ */ jsx("div", {
|
|
544
|
+
ref: props.sentinelRef,
|
|
374
545
|
style: "width:0;height:0;overflow:hidden"
|
|
375
546
|
});
|
|
376
|
-
|
|
377
|
-
}
|
|
547
|
+
});
|
|
378
548
|
|
|
379
549
|
//#endregion
|
|
380
550
|
//#region src/i18n-routing.ts
|
|
@@ -948,5 +1118,5 @@ function aiPlugin(..._) {
|
|
|
948
1118
|
}
|
|
949
1119
|
|
|
950
1120
|
//#endregion
|
|
951
|
-
export { Image, Link, Meta, Script, ThemeToggle, aiPlugin, buildLocalePath, buildMetaTags, createLink, createServer, defineConfig, extractLocaleFromPath, faviconPlugin, initTheme, ogImagePlugin, prefetchRoute, resolvedTheme, seoPlugin, setLocale, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme, useLink, useLocale, validateEnv };
|
|
1121
|
+
export { Image, Link, Meta, Script, ThemeToggle, aiPlugin, buildLocalePath, buildMetaTags, createImage, createLink, createScript, createServer, defineConfig, extractLocaleFromPath, faviconPlugin, initTheme, ogImagePlugin, prefetchRoute, resolvedTheme, seoPlugin, setLocale, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme, useImage, useLink, useLocale, useScript, validateEnv };
|
|
952
1122
|
//# sourceMappingURL=index.js.map
|
package/lib/og-image.js
CHANGED
|
@@ -71,14 +71,14 @@ function buildTextOverlaySvg(layers, width, height, locale) {
|
|
|
71
71
|
const lines = [];
|
|
72
72
|
let currentLine = "";
|
|
73
73
|
const estimateWidth = (s) => {
|
|
74
|
-
let
|
|
74
|
+
let w = 0;
|
|
75
75
|
for (let i = 0; i < s.length; i++) {
|
|
76
76
|
const code = s.charCodeAt(i);
|
|
77
|
-
if (code >= 12288 && code <= 40959)
|
|
78
|
-
else if (code <= 126 && "iljft!|:;.,'".includes(s[i]))
|
|
79
|
-
else
|
|
77
|
+
if (code >= 12288 && code <= 40959) w += fontSize * 1;
|
|
78
|
+
else if (code <= 126 && "iljft!|:;.,'".includes(s[i])) w += fontSize * .35;
|
|
79
|
+
else w += fontSize * .55;
|
|
80
80
|
}
|
|
81
|
-
return
|
|
81
|
+
return w;
|
|
82
82
|
};
|
|
83
83
|
for (const word of words) {
|
|
84
84
|
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) {
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (!no_symbols) {
|
|
12
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
}
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { __exportAll as t };
|
package/lib/script.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRef, onMount, onUnmount } from "@pyreon/core";
|
|
2
|
+
import { signal } from "@pyreon/reactivity";
|
|
2
3
|
import { jsx } from "@pyreon/core/jsx-runtime";
|
|
3
4
|
|
|
4
5
|
//#region src/utils/use-intersection-observer.ts
|
|
@@ -28,36 +29,67 @@ function useIntersectionObserver(getElement, onIntersect, rootMargin = "200px")
|
|
|
28
29
|
//#endregion
|
|
29
30
|
//#region src/script.tsx
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
+
* Composable that provides all script loading behavior — strategy state
|
|
33
|
+
* machine (afterHydration / onIdle / onInteraction / onViewport),
|
|
34
|
+
* deduplication, load/error tracking.
|
|
32
35
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* // Load chat widget when user scrolls
|
|
38
|
-
* <Script src="/chat-widget.js" strategy="onViewport" />
|
|
36
|
+
* Returns reactive signals (`loaded`, `errored`, `pending`) so consumers
|
|
37
|
+
* can render loading indicators, retry buttons, or analytics-readiness
|
|
38
|
+
* gates without re-implementing the strategy machine.
|
|
39
39
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* @example
|
|
41
|
+
* function MyScript(props: ScriptProps) {
|
|
42
|
+
* const s = useScript(props)
|
|
43
|
+
* return (
|
|
44
|
+
* <>
|
|
45
|
+
* {() => s.loaded() ? <Analytics /> : <Skeleton />}
|
|
46
|
+
* {() => s.needsSentinel && <div ref={s.sentinelRef} style="width:0;height:0" />}
|
|
47
|
+
* </>
|
|
48
|
+
* )
|
|
49
|
+
* }
|
|
44
50
|
*/
|
|
45
|
-
function
|
|
51
|
+
function useScript(props) {
|
|
52
|
+
const strategy = props.strategy ?? "afterHydration";
|
|
53
|
+
const loaded = signal(false);
|
|
54
|
+
const errored = signal(false);
|
|
55
|
+
const pending = signal(strategy !== "beforeHydration" && strategy !== "afterHydration");
|
|
56
|
+
const sentinelRef = strategy === "onViewport" ? createRef() : void 0;
|
|
46
57
|
function loadScript() {
|
|
47
58
|
if (typeof document === "undefined") return;
|
|
48
|
-
if (props.id && document.getElementById(props.id))
|
|
59
|
+
if (props.id && document.getElementById(props.id)) {
|
|
60
|
+
loaded.set(true);
|
|
61
|
+
pending.set(false);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
49
64
|
const script = document.createElement("script");
|
|
50
65
|
if (props.src) script.src = props.src;
|
|
51
66
|
if (props.id) script.id = props.id;
|
|
52
67
|
script.async = props.async !== false;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
script.onload = () => {
|
|
69
|
+
loaded.set(true);
|
|
70
|
+
pending.set(false);
|
|
71
|
+
props.onLoad?.();
|
|
72
|
+
};
|
|
73
|
+
script.onerror = () => {
|
|
74
|
+
errored.set(true);
|
|
75
|
+
pending.set(false);
|
|
76
|
+
props.onError?.(/* @__PURE__ */ new Error(`Failed to load: ${props.src}`));
|
|
77
|
+
};
|
|
78
|
+
if (props.children && !props.src) {
|
|
79
|
+
script.textContent = props.children;
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
loaded.set(true);
|
|
82
|
+
pending.set(false);
|
|
83
|
+
}, 0);
|
|
84
|
+
}
|
|
56
85
|
document.head.appendChild(script);
|
|
57
86
|
}
|
|
58
87
|
onMount(() => {
|
|
59
|
-
switch (
|
|
60
|
-
case "beforeHydration":
|
|
88
|
+
switch (strategy) {
|
|
89
|
+
case "beforeHydration":
|
|
90
|
+
loaded.set(true);
|
|
91
|
+
pending.set(false);
|
|
92
|
+
break;
|
|
61
93
|
case "afterHydration":
|
|
62
94
|
loadScript();
|
|
63
95
|
break;
|
|
@@ -88,16 +120,73 @@ function Script(props) {
|
|
|
88
120
|
case "onViewport": break;
|
|
89
121
|
}
|
|
90
122
|
});
|
|
91
|
-
const sentinelRef = createRef();
|
|
92
|
-
const strategy = props.strategy ?? "afterHydration";
|
|
93
123
|
if (strategy === "onViewport") useIntersectionObserver(() => sentinelRef.current ?? void 0, () => loadScript());
|
|
94
|
-
|
|
95
|
-
|
|
124
|
+
return {
|
|
125
|
+
sentinelRef,
|
|
126
|
+
loaded,
|
|
127
|
+
errored,
|
|
128
|
+
pending,
|
|
129
|
+
needsSentinel: strategy === "onViewport",
|
|
130
|
+
load: loadScript
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Higher-order component that wraps any component with script load behavior.
|
|
135
|
+
*
|
|
136
|
+
* The wrapped component receives {@link ScriptRenderProps} with the sentinel
|
|
137
|
+
* ref, load-state signals, and a `needsSentinel` flag. Use this when you want
|
|
138
|
+
* to render a loading indicator, retry button, or custom analytics-readiness
|
|
139
|
+
* gate around the script load.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* // Script with a loading indicator
|
|
143
|
+
* const TrackedScript = createScript((props) => (
|
|
144
|
+
* <>
|
|
145
|
+
* {() => props.pending() && <Spinner />}
|
|
146
|
+
* {() => props.errored() && <button onClick={() => location.reload()}>Retry</button>}
|
|
147
|
+
* {props.needsSentinel && <div ref={props.sentinelRef} style="width:0;height:0" />}
|
|
148
|
+
* </>
|
|
149
|
+
* ))
|
|
150
|
+
*
|
|
151
|
+
* <TrackedScript src="/analytics.js" strategy="onIdle" />
|
|
152
|
+
*/
|
|
153
|
+
function createScript(Component) {
|
|
154
|
+
return function WrappedScript(props) {
|
|
155
|
+
const s = useScript(props);
|
|
156
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
157
|
+
sentinelRef: s.sentinelRef,
|
|
158
|
+
needsSentinel: s.needsSentinel,
|
|
159
|
+
loaded: s.loaded,
|
|
160
|
+
errored: s.errored,
|
|
161
|
+
pending: s.pending
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Default optimized script component. Renders a 0×0 sentinel `<div>` for the
|
|
167
|
+
* `onViewport` strategy (so IntersectionObserver has an element to observe),
|
|
168
|
+
* `null` for every other strategy.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* // Load analytics after page is interactive
|
|
172
|
+
* <Script src="https://analytics.example.com/script.js" strategy="onIdle" />
|
|
173
|
+
*
|
|
174
|
+
* // Load chat widget when user scrolls
|
|
175
|
+
* <Script src="/chat-widget.js" strategy="onViewport" />
|
|
176
|
+
*
|
|
177
|
+
* // Inline script with deferred execution
|
|
178
|
+
* <Script strategy="afterHydration">
|
|
179
|
+
* {`console.log("App hydrated!")`}
|
|
180
|
+
* <\/Script>
|
|
181
|
+
*/
|
|
182
|
+
const Script = createScript((props) => {
|
|
183
|
+
if (!props.needsSentinel) return null;
|
|
184
|
+
return /* @__PURE__ */ jsx("div", {
|
|
185
|
+
ref: props.sentinelRef,
|
|
96
186
|
style: "width:0;height:0;overflow:hidden"
|
|
97
187
|
});
|
|
98
|
-
|
|
99
|
-
}
|
|
188
|
+
});
|
|
100
189
|
|
|
101
190
|
//#endregion
|
|
102
|
-
export { Script };
|
|
191
|
+
export { Script, createScript, useScript };
|
|
103
192
|
//# sourceMappingURL=script.js.map
|