@pyreon/zero 0.5.0 → 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.
- package/lib/cache.js.map +1 -1
- package/lib/client.js.map +1 -1
- package/lib/config.js.map +1 -1
- package/lib/font.js.map +1 -1
- package/lib/fs-router-BkbIWqek.js.map +1 -1
- package/lib/fs-router-n4VA4lxu.js.map +1 -1
- package/lib/image-plugin.js.map +1 -1
- package/lib/image.js +1 -1
- package/lib/image.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/link.js +1 -1
- package/lib/link.js.map +1 -1
- package/lib/script.js +1 -1
- package/lib/script.js.map +1 -1
- package/lib/seo.js.map +1 -1
- package/lib/theme.js +1 -1
- package/lib/theme.js.map +1 -1
- package/package.json +14 -13
- package/src/actions.ts +20 -28
- package/src/adapters/bun.ts +7 -7
- package/src/adapters/index.ts +12 -14
- package/src/adapters/node.ts +8 -11
- package/src/adapters/static.ts +3 -3
- package/src/api-routes.ts +23 -50
- package/src/app.ts +9 -13
- package/src/cache.ts +16 -29
- package/src/client.ts +8 -8
- package/src/compression.ts +21 -28
- package/src/config.ts +6 -7
- package/src/cors.ts +20 -28
- package/src/entry-server.ts +15 -19
- package/src/error-overlay.ts +10 -13
- package/src/font.ts +44 -55
- package/src/fs-router.ts +44 -63
- package/src/image-plugin.ts +53 -79
- package/src/image.tsx +39 -41
- package/src/index.ts +36 -36
- package/src/isr.ts +8 -8
- package/src/link.tsx +27 -30
- package/src/rate-limit.ts +15 -15
- package/src/script.tsx +21 -22
- package/src/seo.ts +47 -57
- package/src/sharp.d.ts +2 -6
- package/src/testing.ts +8 -12
- package/src/theme.tsx +18 -20
- package/src/types.ts +6 -6
- package/src/utils/use-intersection-observer.ts +2 -2
- package/src/utils/with-headers.ts +1 -4
- package/src/vite-plugin.ts +21 -28
- package/lib/types/actions.d.ts +0 -57
- package/lib/types/actions.d.ts.map +0 -1
- package/lib/types/adapters/bun.d.ts +0 -6
- package/lib/types/adapters/bun.d.ts.map +0 -1
- package/lib/types/adapters/index.d.ts +0 -10
- package/lib/types/adapters/index.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/api-routes.d.ts +0 -66
- package/lib/types/api-routes.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/cache.d.ts +0 -54
- package/lib/types/cache.d.ts.map +0 -1
- package/lib/types/client.d.ts +0 -19
- package/lib/types/client.d.ts.map +0 -1
- package/lib/types/compression.d.ts +0 -33
- package/lib/types/compression.d.ts.map +0 -1
- package/lib/types/config.d.ts +0 -18
- package/lib/types/config.d.ts.map +0 -1
- package/lib/types/cors.d.ts +0 -32
- package/lib/types/cors.d.ts.map +0 -1
- package/lib/types/entry-server.d.ts +0 -34
- 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/font.d.ts +0 -119
- package/lib/types/font.d.ts.map +0 -1
- package/lib/types/fs-router.d.ts +0 -38
- package/lib/types/fs-router.d.ts.map +0 -1
- package/lib/types/image-plugin.d.ts +0 -79
- package/lib/types/image-plugin.d.ts.map +0 -1
- package/lib/types/image.d.ts +0 -51
- package/lib/types/image.d.ts.map +0 -1
- package/lib/types/index.d.ts +0 -37
- package/lib/types/index.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/link.d.ts +0 -116
- package/lib/types/link.d.ts.map +0 -1
- package/lib/types/rate-limit.d.ts +0 -34
- package/lib/types/rate-limit.d.ts.map +0 -1
- package/lib/types/script.d.ts +0 -35
- package/lib/types/script.d.ts.map +0 -1
- package/lib/types/seo.d.ts +0 -88
- package/lib/types/seo.d.ts.map +0 -1
- package/lib/types/testing.d.ts +0 -85
- package/lib/types/testing.d.ts.map +0 -1
- package/lib/types/theme.d.ts +0 -39
- package/lib/types/theme.d.ts.map +0 -1
- package/lib/types/types.d.ts +0 -109
- 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/src/image-plugin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync } from
|
|
2
|
-
import { mkdir, readFile, writeFile } from
|
|
3
|
-
import { basename, extname, join } from
|
|
4
|
-
import type { Plugin } from
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
3
|
+
import { basename, extname, join } from "node:path"
|
|
4
|
+
import type { Plugin } from "vite"
|
|
5
5
|
|
|
6
6
|
let sharpWarned = false
|
|
7
7
|
function warnSharpMissing() {
|
|
@@ -9,7 +9,7 @@ function warnSharpMissing() {
|
|
|
9
9
|
sharpWarned = true
|
|
10
10
|
// biome-ignore lint/suspicious/noConsole: intentional build-time warning
|
|
11
11
|
console.warn(
|
|
12
|
-
|
|
12
|
+
"\n[zero:image] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\n",
|
|
13
13
|
)
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -44,7 +44,7 @@ export interface ImagePluginConfig {
|
|
|
44
44
|
include?: RegExp
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export type ImageFormat =
|
|
47
|
+
export type ImageFormat = "webp" | "avif" | "jpeg" | "png"
|
|
48
48
|
|
|
49
49
|
/** Per-format source set for <picture> <source> elements. */
|
|
50
50
|
export interface FormatSource {
|
|
@@ -99,42 +99,39 @@ const IMAGE_EXT_RE = /\.(jpe?g|png|webp|avif)$/i
|
|
|
99
99
|
*/
|
|
100
100
|
export function imagePlugin(config: ImagePluginConfig = {}): Plugin {
|
|
101
101
|
const defaultWidths = config.widths ?? [640, 1024, 1920]
|
|
102
|
-
const defaultFormats = config.formats ?? [
|
|
102
|
+
const defaultFormats = config.formats ?? ["webp"]
|
|
103
103
|
const quality = config.quality ?? 80
|
|
104
104
|
const placeholderSize = config.placeholderSize ?? 16
|
|
105
|
-
const outSubDir = config.outDir ??
|
|
105
|
+
const outSubDir = config.outDir ?? "assets/img"
|
|
106
106
|
const include = config.include ?? IMAGE_EXT_RE
|
|
107
107
|
|
|
108
|
-
let root =
|
|
109
|
-
let outDir =
|
|
108
|
+
let root = ""
|
|
109
|
+
let outDir = ""
|
|
110
110
|
let isBuild = false
|
|
111
111
|
|
|
112
112
|
return {
|
|
113
|
-
name:
|
|
114
|
-
enforce:
|
|
113
|
+
name: "pyreon-zero-images",
|
|
114
|
+
enforce: "pre",
|
|
115
115
|
|
|
116
116
|
configResolved(resolvedConfig) {
|
|
117
117
|
root = resolvedConfig.root
|
|
118
118
|
outDir = resolvedConfig.build.outDir
|
|
119
|
-
isBuild = resolvedConfig.command ===
|
|
119
|
+
isBuild = resolvedConfig.command === "build"
|
|
120
120
|
},
|
|
121
121
|
|
|
122
122
|
async resolveId(id) {
|
|
123
123
|
// Handle ?optimize query on image imports
|
|
124
|
-
if (id.includes(
|
|
124
|
+
if (id.includes("?optimize") && include.test(id.split("?")[0]!)) {
|
|
125
125
|
return `\0virtual:zero-image:${id}`
|
|
126
126
|
}
|
|
127
127
|
return null
|
|
128
128
|
},
|
|
129
129
|
|
|
130
130
|
async load(id) {
|
|
131
|
-
if (!id.startsWith(
|
|
131
|
+
if (!id.startsWith("\0virtual:zero-image:")) return null
|
|
132
132
|
|
|
133
|
-
const rawPath =
|
|
134
|
-
|
|
135
|
-
const absPath = rawPath.startsWith('/')
|
|
136
|
-
? join(root, 'public', rawPath)
|
|
137
|
-
: rawPath
|
|
133
|
+
const rawPath = id.replace("\0virtual:zero-image:", "").split("?")[0] ?? id
|
|
134
|
+
const absPath = rawPath.startsWith("/") ? join(root, "public", rawPath) : rawPath
|
|
138
135
|
|
|
139
136
|
if (!isBuild) {
|
|
140
137
|
const result = await loadDevImage(absPath, rawPath, placeholderSize)
|
|
@@ -164,16 +161,16 @@ async function loadDevImage(
|
|
|
164
161
|
placeholderSize: number,
|
|
165
162
|
): Promise<ProcessedImage> {
|
|
166
163
|
const metadata = await getImageMetadata(absPath)
|
|
167
|
-
const publicPath = rawPath.startsWith(
|
|
164
|
+
const publicPath = rawPath.startsWith("/") ? rawPath : `/@fs/${absPath}`
|
|
168
165
|
|
|
169
166
|
return {
|
|
170
167
|
src: publicPath,
|
|
171
|
-
srcset:
|
|
168
|
+
srcset: "",
|
|
172
169
|
width: metadata.width,
|
|
173
170
|
height: metadata.height,
|
|
174
171
|
placeholder: await generateBlurPlaceholder(absPath, placeholderSize),
|
|
175
172
|
formats: [],
|
|
176
|
-
sources: [{ src: publicPath, width: metadata.width, format:
|
|
173
|
+
sources: [{ src: publicPath, width: metadata.width, format: "original" }],
|
|
177
174
|
}
|
|
178
175
|
}
|
|
179
176
|
|
|
@@ -181,17 +178,13 @@ async function emitProcessedSources(
|
|
|
181
178
|
processed: ProcessedImage,
|
|
182
179
|
outSubDir: string,
|
|
183
180
|
ctx: {
|
|
184
|
-
emitFile: (f: {
|
|
185
|
-
type: 'asset'
|
|
186
|
-
fileName: string
|
|
187
|
-
source: Uint8Array
|
|
188
|
-
}) => void
|
|
181
|
+
emitFile: (f: { type: "asset"; fileName: string; source: Uint8Array }) => void
|
|
189
182
|
},
|
|
190
183
|
) {
|
|
191
184
|
for (const source of processed.sources) {
|
|
192
185
|
const fileName = join(outSubDir, basename(source.src))
|
|
193
186
|
const content = await readFile(source.src)
|
|
194
|
-
ctx.emitFile({ type:
|
|
187
|
+
ctx.emitFile({ type: "asset", fileName, source: content })
|
|
195
188
|
source.src = `/${fileName}`
|
|
196
189
|
}
|
|
197
190
|
}
|
|
@@ -208,11 +201,11 @@ function rebuildFormatSrcsets(processed: ProcessedImage, fallbackPath: string) {
|
|
|
208
201
|
}
|
|
209
202
|
processed.formats = [...formatGroups.entries()].map(([fmt, entries]) => ({
|
|
210
203
|
type: `image/${fmt}`,
|
|
211
|
-
srcset: entries.join(
|
|
204
|
+
srcset: entries.join(", "),
|
|
212
205
|
}))
|
|
213
206
|
|
|
214
207
|
const lastFormat = processed.formats.at(-1)
|
|
215
|
-
processed.srcset = lastFormat?.srcset ??
|
|
208
|
+
processed.srcset = lastFormat?.srcset ?? ""
|
|
216
209
|
processed.src = processed.sources.at(-1)?.src ?? fallbackPath
|
|
217
210
|
}
|
|
218
211
|
|
|
@@ -227,10 +220,7 @@ interface ProcessOptions {
|
|
|
227
220
|
outDir: string
|
|
228
221
|
}
|
|
229
222
|
|
|
230
|
-
async function processImage(
|
|
231
|
-
absPath: string,
|
|
232
|
-
opts: ProcessOptions,
|
|
233
|
-
): Promise<ProcessedImage> {
|
|
223
|
+
async function processImage(absPath: string, opts: ProcessOptions): Promise<ProcessedImage> {
|
|
234
224
|
const metadata = await getImageMetadata(absPath)
|
|
235
225
|
const ext = extname(absPath)
|
|
236
226
|
const name = basename(absPath, ext)
|
|
@@ -266,26 +256,21 @@ async function processImage(
|
|
|
266
256
|
group.push({ src: s.src, width: s.width })
|
|
267
257
|
}
|
|
268
258
|
|
|
269
|
-
const formats: FormatSource[] = [...formatGroups.entries()].map(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}),
|
|
274
|
-
)
|
|
259
|
+
const formats: FormatSource[] = [...formatGroups.entries()].map(([fmt, group]) => ({
|
|
260
|
+
type: `image/${fmt === "jpeg" ? "jpeg" : fmt}`,
|
|
261
|
+
srcset: group.map((s) => `${s.src} ${s.width}w`).join(", "),
|
|
262
|
+
}))
|
|
275
263
|
|
|
276
264
|
// Fallback: last format's srcset
|
|
277
265
|
const fallbackFormat = formats[formats.length - 1]
|
|
278
266
|
const fallbackSources = formatGroups.get([...formatGroups.keys()].pop()!)!
|
|
279
267
|
|
|
280
268
|
// Generate blur placeholder
|
|
281
|
-
const placeholder = await generateBlurPlaceholder(
|
|
282
|
-
absPath,
|
|
283
|
-
opts.placeholderSize,
|
|
284
|
-
)
|
|
269
|
+
const placeholder = await generateBlurPlaceholder(absPath, opts.placeholderSize)
|
|
285
270
|
|
|
286
271
|
return {
|
|
287
272
|
src: fallbackSources[fallbackSources.length - 1]?.src ?? absPath,
|
|
288
|
-
srcset: fallbackFormat?.srcset ??
|
|
273
|
+
srcset: fallbackFormat?.srcset ?? "",
|
|
289
274
|
width: metadata.width,
|
|
290
275
|
height: metadata.height,
|
|
291
276
|
placeholder,
|
|
@@ -308,23 +293,23 @@ async function getImageMetadata(absPath: string): Promise<ImageMetadata> {
|
|
|
308
293
|
const buffer = await readFile(absPath)
|
|
309
294
|
const ext = extname(absPath).toLowerCase()
|
|
310
295
|
|
|
311
|
-
if (ext ===
|
|
296
|
+
if (ext === ".png") {
|
|
312
297
|
// PNG: width at bytes 16-19, height at 20-23 (big-endian)
|
|
313
298
|
const width = buffer.readUInt32BE(16)
|
|
314
299
|
const height = buffer.readUInt32BE(20)
|
|
315
|
-
return { width, height, format:
|
|
300
|
+
return { width, height, format: "png" }
|
|
316
301
|
}
|
|
317
302
|
|
|
318
|
-
if (ext ===
|
|
303
|
+
if (ext === ".jpg" || ext === ".jpeg") {
|
|
319
304
|
// JPEG: scan for SOF markers
|
|
320
305
|
const dimensions = parseJpegDimensions(buffer)
|
|
321
|
-
return { ...dimensions, format:
|
|
306
|
+
return { ...dimensions, format: "jpeg" }
|
|
322
307
|
}
|
|
323
308
|
|
|
324
|
-
if (ext ===
|
|
309
|
+
if (ext === ".webp") {
|
|
325
310
|
// WebP: VP8 header
|
|
326
311
|
const dimensions = parseWebPDimensions(buffer)
|
|
327
|
-
return { ...dimensions, format:
|
|
312
|
+
return { ...dimensions, format: "webp" }
|
|
328
313
|
}
|
|
329
314
|
|
|
330
315
|
// Fallback
|
|
@@ -341,13 +326,7 @@ export function parseJpegDimensions(buffer: Buffer): {
|
|
|
341
326
|
if (buffer[offset] !== 0xff) break
|
|
342
327
|
const marker = buffer[offset + 1]!
|
|
343
328
|
// SOF markers (0xC0-0xCF except 0xC4, 0xC8, 0xCC)
|
|
344
|
-
if (
|
|
345
|
-
marker >= 0xc0 &&
|
|
346
|
-
marker <= 0xcf &&
|
|
347
|
-
marker !== 0xc4 &&
|
|
348
|
-
marker !== 0xc8 &&
|
|
349
|
-
marker !== 0xcc
|
|
350
|
-
) {
|
|
329
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
351
330
|
const height = buffer.readUInt16BE(offset + 5)
|
|
352
331
|
const width = buffer.readUInt16BE(offset + 7)
|
|
353
332
|
return { width, height }
|
|
@@ -364,26 +343,24 @@ export function parseWebPDimensions(buffer: Buffer): {
|
|
|
364
343
|
height: number
|
|
365
344
|
} {
|
|
366
345
|
// RIFF header: bytes 0-3 = "RIFF", 8-11 = "WEBP"
|
|
367
|
-
const chunk = buffer.toString(
|
|
368
|
-
if (chunk ===
|
|
346
|
+
const chunk = buffer.toString("ascii", 12, 16)
|
|
347
|
+
if (chunk === "VP8 ") {
|
|
369
348
|
// Lossy VP8
|
|
370
349
|
const width = buffer.readUInt16LE(26) & 0x3fff
|
|
371
350
|
const height = buffer.readUInt16LE(28) & 0x3fff
|
|
372
351
|
return { width, height }
|
|
373
352
|
}
|
|
374
|
-
if (chunk ===
|
|
353
|
+
if (chunk === "VP8L") {
|
|
375
354
|
// Lossless VP8L
|
|
376
355
|
const bits = buffer.readUInt32LE(21)
|
|
377
356
|
const width = (bits & 0x3fff) + 1
|
|
378
357
|
const height = ((bits >> 14) & 0x3fff) + 1
|
|
379
358
|
return { width, height }
|
|
380
359
|
}
|
|
381
|
-
if (chunk ===
|
|
360
|
+
if (chunk === "VP8X") {
|
|
382
361
|
// Extended VP8X
|
|
383
|
-
const width =
|
|
384
|
-
|
|
385
|
-
const height =
|
|
386
|
-
1 + ((buffer[27]! | (buffer[28]! << 8) | (buffer[29]! << 16)) & 0xffffff)
|
|
362
|
+
const width = 1 + ((buffer[24]! | (buffer[25]! << 8) | (buffer[26]! << 16)) & 0xffffff)
|
|
363
|
+
const height = 1 + ((buffer[27]! | (buffer[28]! << 8) | (buffer[29]! << 16)) & 0xffffff)
|
|
387
364
|
return { width, height }
|
|
388
365
|
}
|
|
389
366
|
return { width: 0, height: 0 }
|
|
@@ -402,20 +379,20 @@ async function resizeImage(
|
|
|
402
379
|
): Promise<void> {
|
|
403
380
|
try {
|
|
404
381
|
// Try sharp (the standard Node.js image processing library)
|
|
405
|
-
const sharp = await import(
|
|
382
|
+
const sharp = await import("sharp").then((m) => m.default ?? m)
|
|
406
383
|
let pipeline = sharp(input).resize(width)
|
|
407
384
|
|
|
408
385
|
switch (format) {
|
|
409
|
-
case
|
|
386
|
+
case "webp":
|
|
410
387
|
pipeline = pipeline.webp({ quality })
|
|
411
388
|
break
|
|
412
|
-
case
|
|
389
|
+
case "avif":
|
|
413
390
|
pipeline = pipeline.avif({ quality })
|
|
414
391
|
break
|
|
415
|
-
case
|
|
392
|
+
case "jpeg":
|
|
416
393
|
pipeline = pipeline.jpeg({ quality, mozjpeg: true })
|
|
417
394
|
break
|
|
418
|
-
case
|
|
395
|
+
case "png":
|
|
419
396
|
pipeline = pipeline.png({ compressionLevel: 9 })
|
|
420
397
|
break
|
|
421
398
|
}
|
|
@@ -432,19 +409,16 @@ async function resizeImage(
|
|
|
432
409
|
/**
|
|
433
410
|
* Generate a tiny blur placeholder as a base64 data URI.
|
|
434
411
|
*/
|
|
435
|
-
async function generateBlurPlaceholder(
|
|
436
|
-
input: string,
|
|
437
|
-
size: number,
|
|
438
|
-
): Promise<string> {
|
|
412
|
+
async function generateBlurPlaceholder(input: string, size: number): Promise<string> {
|
|
439
413
|
try {
|
|
440
|
-
const sharp = await import(
|
|
414
|
+
const sharp = await import("sharp").then((m) => m.default ?? m)
|
|
441
415
|
const buffer = await sharp(input)
|
|
442
|
-
.resize(size, size, { fit:
|
|
416
|
+
.resize(size, size, { fit: "inside" })
|
|
443
417
|
.blur(2)
|
|
444
418
|
.webp({ quality: 20 })
|
|
445
419
|
.toBuffer()
|
|
446
420
|
|
|
447
|
-
return `data:image/webp;base64,${buffer.toString(
|
|
421
|
+
return `data:image/webp;base64,${buffer.toString("base64")}`
|
|
448
422
|
} catch {
|
|
449
423
|
// sharp not available — return a transparent placeholder
|
|
450
424
|
return "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3C/svg%3E"
|
package/src/image.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { VNodeChild } from
|
|
2
|
-
import { createRef } from
|
|
3
|
-
import { signal } from
|
|
4
|
-
import type { FormatSource } from
|
|
5
|
-
import { useIntersectionObserver } from
|
|
1
|
+
import type { VNodeChild } from "@pyreon/core"
|
|
2
|
+
import { createRef } from "@pyreon/core"
|
|
3
|
+
import { signal } from "@pyreon/reactivity"
|
|
4
|
+
import type { FormatSource } from "./image-plugin"
|
|
5
|
+
import { useIntersectionObserver } from "./utils/use-intersection-observer"
|
|
6
6
|
|
|
7
7
|
// ─── Image optimization component ───────────────────────────────────────────
|
|
8
8
|
//
|
|
@@ -30,7 +30,7 @@ export interface ImageProps {
|
|
|
30
30
|
/** Per-format source sets for <picture>. Provided automatically by imagePlugin. */
|
|
31
31
|
formats?: FormatSource[]
|
|
32
32
|
/** Loading strategy. "lazy" uses IntersectionObserver, "eager" loads immediately. Default: "lazy" */
|
|
33
|
-
loading?:
|
|
33
|
+
loading?: "lazy" | "eager"
|
|
34
34
|
/** Mark as priority (LCP image). Disables lazy loading, adds fetchPriority="high". */
|
|
35
35
|
priority?: boolean
|
|
36
36
|
/** Low-quality placeholder image URL or base64 data URI for blur-up effect. */
|
|
@@ -40,9 +40,9 @@ export interface ImageProps {
|
|
|
40
40
|
/** Inline styles. */
|
|
41
41
|
style?: string
|
|
42
42
|
/** CSS object-fit. Default: "cover" */
|
|
43
|
-
fit?:
|
|
43
|
+
fit?: "cover" | "contain" | "fill" | "none" | "scale-down"
|
|
44
44
|
/** Decode async. Default: true */
|
|
45
|
-
decoding?:
|
|
45
|
+
decoding?: "sync" | "async" | "auto"
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export interface ImageSource {
|
|
@@ -64,19 +64,19 @@ export interface ImageSource {
|
|
|
64
64
|
* <Image src="/hero.jpg" alt="Hero" width={1200} height={630} />
|
|
65
65
|
*/
|
|
66
66
|
export function Image(props: ImageProps): VNodeChild {
|
|
67
|
-
const isEager = props.priority || props.loading ===
|
|
67
|
+
const isEager = props.priority || props.loading === "eager"
|
|
68
68
|
const loaded = signal(isEager)
|
|
69
69
|
const inView = signal(isEager)
|
|
70
70
|
const containerRef = createRef<HTMLElement>()
|
|
71
71
|
|
|
72
72
|
// Resolve srcset from string or array
|
|
73
73
|
const resolvedSrcset =
|
|
74
|
-
typeof props.srcset ===
|
|
74
|
+
typeof props.srcset === "string"
|
|
75
75
|
? props.srcset
|
|
76
|
-
: props.srcset?.map((s) => `${s.src} ${s.width}w`).join(
|
|
76
|
+
: props.srcset?.map((s) => `${s.src} ${s.width}w`).join(", ")
|
|
77
77
|
|
|
78
|
-
const sizes = props.sizes ??
|
|
79
|
-
const fit = props.fit ??
|
|
78
|
+
const sizes = props.sizes ?? "100vw"
|
|
79
|
+
const fit = props.fit ?? "cover"
|
|
80
80
|
const hasFormats = props.formats && props.formats.length > 0
|
|
81
81
|
const aspectRatio = `${props.width} / ${props.height}`
|
|
82
82
|
|
|
@@ -89,39 +89,37 @@ export function Image(props: ImageProps): VNodeChild {
|
|
|
89
89
|
|
|
90
90
|
// Static styles (don't depend on signals)
|
|
91
91
|
const containerStyle = [
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
"position: relative",
|
|
93
|
+
"overflow: hidden",
|
|
94
94
|
`aspect-ratio: ${aspectRatio}`,
|
|
95
95
|
`max-width: ${props.width}px`,
|
|
96
|
-
|
|
96
|
+
"width: 100%",
|
|
97
97
|
props.style,
|
|
98
98
|
]
|
|
99
99
|
.filter(Boolean)
|
|
100
|
-
.join(
|
|
100
|
+
.join("; ")
|
|
101
101
|
|
|
102
102
|
const imgEl = (
|
|
103
103
|
<img
|
|
104
|
-
src={() => (inView() ? props.src :
|
|
105
|
-
srcSet={() =>
|
|
106
|
-
!hasFormats && inView() && resolvedSrcset ? resolvedSrcset : ''
|
|
107
|
-
}
|
|
104
|
+
src={() => (inView() ? props.src : "")}
|
|
105
|
+
srcSet={() => (!hasFormats && inView() && resolvedSrcset ? resolvedSrcset : "")}
|
|
108
106
|
sizes={resolvedSrcset ? sizes : undefined}
|
|
109
107
|
alt={props.alt}
|
|
110
108
|
width={props.width}
|
|
111
109
|
height={props.height}
|
|
112
|
-
loading={isEager ?
|
|
113
|
-
decoding={props.decoding ??
|
|
114
|
-
fetchPriority={props.priority ?
|
|
110
|
+
loading={isEager ? "eager" : "lazy"}
|
|
111
|
+
decoding={props.decoding ?? "async"}
|
|
112
|
+
fetchPriority={props.priority ? "high" : undefined}
|
|
115
113
|
onLoad={() => loaded.set(true)}
|
|
116
114
|
style={() =>
|
|
117
115
|
[
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
"display: block",
|
|
117
|
+
"width: 100%",
|
|
118
|
+
"height: 100%",
|
|
121
119
|
`object-fit: ${fit}`,
|
|
122
|
-
|
|
123
|
-
props.placeholder && !loaded() ?
|
|
124
|
-
].join(
|
|
120
|
+
"transition: opacity 0.3s ease",
|
|
121
|
+
props.placeholder && !loaded() ? "opacity: 0" : "opacity: 1",
|
|
122
|
+
].join("; ")
|
|
125
123
|
}
|
|
126
124
|
/>
|
|
127
125
|
)
|
|
@@ -136,16 +134,16 @@ export function Image(props: ImageProps): VNodeChild {
|
|
|
136
134
|
loading="eager"
|
|
137
135
|
style={() =>
|
|
138
136
|
[
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
loaded() ?
|
|
148
|
-
].join(
|
|
137
|
+
"position: absolute",
|
|
138
|
+
"inset: 0",
|
|
139
|
+
"width: 100%",
|
|
140
|
+
"height: 100%",
|
|
141
|
+
"object-fit: cover",
|
|
142
|
+
"filter: blur(20px)",
|
|
143
|
+
"transform: scale(1.1)",
|
|
144
|
+
"transition: opacity 0.4s ease",
|
|
145
|
+
loaded() ? "opacity: 0; pointer-events: none" : "opacity: 1",
|
|
146
|
+
].join("; ")
|
|
149
147
|
}
|
|
150
148
|
/>
|
|
151
149
|
)}
|
|
@@ -154,7 +152,7 @@ export function Image(props: ImageProps): VNodeChild {
|
|
|
154
152
|
{props.formats?.map((fmt) => (
|
|
155
153
|
<source
|
|
156
154
|
type={fmt.type}
|
|
157
|
-
srcSet={() => inView() ? fmt.srcset ??
|
|
155
|
+
srcSet={() => (inView() ? (fmt.srcset ?? "") : "")}
|
|
158
156
|
sizes={sizes}
|
|
159
157
|
/>
|
|
160
158
|
))}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// ─── Core ─────────────────────────────────────────────────────────────────────
|
|
2
2
|
|
|
3
|
-
export type { CreateAppOptions } from
|
|
4
|
-
export { createApp } from
|
|
5
|
-
export type { CreateServerOptions } from
|
|
6
|
-
export { createServer } from
|
|
3
|
+
export type { CreateAppOptions } from "./app"
|
|
4
|
+
export { createApp } from "./app"
|
|
5
|
+
export type { CreateServerOptions } from "./entry-server"
|
|
6
|
+
export { createServer } from "./entry-server"
|
|
7
7
|
|
|
8
8
|
// ─── Vite plugin ─────────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
export { zeroPlugin as default } from
|
|
10
|
+
export { zeroPlugin as default } from "./vite-plugin"
|
|
11
11
|
|
|
12
12
|
// ─── File-system routing ─────────────────────────────────────────────────────
|
|
13
13
|
|
|
@@ -17,15 +17,15 @@ export {
|
|
|
17
17
|
generateRouteModule,
|
|
18
18
|
parseFileRoutes,
|
|
19
19
|
scanRouteFiles,
|
|
20
|
-
} from
|
|
20
|
+
} from "./fs-router"
|
|
21
21
|
|
|
22
22
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
export { defineConfig, resolveConfig } from
|
|
24
|
+
export { defineConfig, resolveConfig } from "./config"
|
|
25
25
|
|
|
26
26
|
// ─── ISR ─────────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
export { createISRHandler } from
|
|
28
|
+
export { createISRHandler } from "./isr"
|
|
29
29
|
|
|
30
30
|
// ─── Adapters ────────────────────────────────────────────────────────────────
|
|
31
31
|
|
|
@@ -34,21 +34,21 @@ export {
|
|
|
34
34
|
nodeAdapter,
|
|
35
35
|
resolveAdapter,
|
|
36
36
|
staticAdapter,
|
|
37
|
-
} from
|
|
37
|
+
} from "./adapters"
|
|
38
38
|
|
|
39
39
|
// ─── Components ─────────────────────────────────────────────────────────────
|
|
40
40
|
|
|
41
|
-
export type { ImageProps, ImageSource } from
|
|
42
|
-
export { Image } from
|
|
43
|
-
export type { LinkProps, LinkRenderProps, UseLinkReturn } from
|
|
44
|
-
export { createLink, Link, useLink } from
|
|
45
|
-
export type { ScriptProps, ScriptStrategy } from
|
|
46
|
-
export { Script } from
|
|
41
|
+
export type { ImageProps, ImageSource } from "./image"
|
|
42
|
+
export { Image } from "./image"
|
|
43
|
+
export type { LinkProps, LinkRenderProps, UseLinkReturn } from "./link"
|
|
44
|
+
export { createLink, Link, useLink } from "./link"
|
|
45
|
+
export type { ScriptProps, ScriptStrategy } from "./script"
|
|
46
|
+
export { Script } from "./script"
|
|
47
47
|
|
|
48
48
|
// ─── Middleware ──────────────────────────────────────────────────────────────
|
|
49
49
|
|
|
50
|
-
export type { CacheConfig, CacheRule } from
|
|
51
|
-
export { cacheMiddleware, securityHeaders, varyEncoding } from
|
|
50
|
+
export type { CacheConfig, CacheRule } from "./cache"
|
|
51
|
+
export { cacheMiddleware, securityHeaders, varyEncoding } from "./cache"
|
|
52
52
|
|
|
53
53
|
// ─── Font optimization ─────────────────────────────────────────────────────
|
|
54
54
|
|
|
@@ -60,8 +60,8 @@ export type {
|
|
|
60
60
|
GoogleFontStatic,
|
|
61
61
|
GoogleFontVariable,
|
|
62
62
|
LocalFont,
|
|
63
|
-
} from
|
|
64
|
-
export { fontPlugin, fontVariables } from
|
|
63
|
+
} from "./font"
|
|
64
|
+
export { fontPlugin, fontVariables } from "./font"
|
|
65
65
|
|
|
66
66
|
// ─── Image processing ──────────────────────────────────────────────────────
|
|
67
67
|
|
|
@@ -70,12 +70,12 @@ export type {
|
|
|
70
70
|
ImageFormat,
|
|
71
71
|
ImagePluginConfig,
|
|
72
72
|
ProcessedImage,
|
|
73
|
-
} from
|
|
74
|
-
export { imagePlugin } from
|
|
73
|
+
} from "./image-plugin"
|
|
74
|
+
export { imagePlugin } from "./image-plugin"
|
|
75
75
|
|
|
76
76
|
// ─── Theme ──────────────────────────────────────────────────────────────────
|
|
77
77
|
|
|
78
|
-
export type { Theme } from
|
|
78
|
+
export type { Theme } from "./theme"
|
|
79
79
|
export {
|
|
80
80
|
initTheme,
|
|
81
81
|
resolvedTheme,
|
|
@@ -84,7 +84,7 @@ export {
|
|
|
84
84
|
theme,
|
|
85
85
|
themeScript,
|
|
86
86
|
toggleTheme,
|
|
87
|
-
} from
|
|
87
|
+
} from "./theme"
|
|
88
88
|
|
|
89
89
|
// ─── SEO ────────────────────────────────────────────────────────────────────
|
|
90
90
|
|
|
@@ -96,14 +96,14 @@ export type {
|
|
|
96
96
|
SeoPluginConfig,
|
|
97
97
|
SitemapConfig,
|
|
98
98
|
SitemapEntry,
|
|
99
|
-
} from
|
|
99
|
+
} from "./seo"
|
|
100
100
|
export {
|
|
101
101
|
generateRobots,
|
|
102
102
|
generateSitemap,
|
|
103
103
|
jsonLd,
|
|
104
104
|
seoMiddleware,
|
|
105
105
|
seoPlugin,
|
|
106
|
-
} from
|
|
106
|
+
} from "./seo"
|
|
107
107
|
|
|
108
108
|
// ─── API routes ──────────────────────────────────────────────────────────────
|
|
109
109
|
|
|
@@ -113,32 +113,32 @@ export type {
|
|
|
113
113
|
ApiRouteEntry,
|
|
114
114
|
ApiRouteModule,
|
|
115
115
|
HttpMethod,
|
|
116
|
-
} from
|
|
117
|
-
export { createApiMiddleware, generateApiRouteModule } from
|
|
116
|
+
} from "./api-routes"
|
|
117
|
+
export { createApiMiddleware, generateApiRouteModule } from "./api-routes"
|
|
118
118
|
|
|
119
119
|
// ─── CORS ────────────────────────────────────────────────────────────────────
|
|
120
120
|
|
|
121
|
-
export type { CorsConfig } from
|
|
122
|
-
export { corsMiddleware } from
|
|
121
|
+
export type { CorsConfig } from "./cors"
|
|
122
|
+
export { corsMiddleware } from "./cors"
|
|
123
123
|
|
|
124
124
|
// ─── Rate limiting ──────────────────────────────────────────────────────────
|
|
125
125
|
|
|
126
|
-
export type { RateLimitConfig } from
|
|
127
|
-
export { rateLimitMiddleware } from
|
|
126
|
+
export type { RateLimitConfig } from "./rate-limit"
|
|
127
|
+
export { rateLimitMiddleware } from "./rate-limit"
|
|
128
128
|
|
|
129
129
|
// ─── Compression ────────────────────────────────────────────────────────────
|
|
130
130
|
|
|
131
|
-
export type { CompressionConfig } from
|
|
131
|
+
export type { CompressionConfig } from "./compression"
|
|
132
132
|
export {
|
|
133
133
|
compressionMiddleware,
|
|
134
134
|
compressResponse,
|
|
135
135
|
isCompressible,
|
|
136
|
-
} from
|
|
136
|
+
} from "./compression"
|
|
137
137
|
|
|
138
138
|
// ─── Actions ─────────────────────────────────────────────────────────────────
|
|
139
139
|
|
|
140
|
-
export type { Action, ActionContext, ActionHandler } from
|
|
141
|
-
export { createActionMiddleware, defineAction } from
|
|
140
|
+
export type { Action, ActionContext, ActionHandler } from "./actions"
|
|
141
|
+
export { createActionMiddleware, defineAction } from "./actions"
|
|
142
142
|
|
|
143
143
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
144
144
|
|
|
@@ -153,4 +153,4 @@ export type {
|
|
|
153
153
|
RouteMiddlewareEntry,
|
|
154
154
|
RouteModule,
|
|
155
155
|
ZeroConfig,
|
|
156
|
-
} from
|
|
156
|
+
} from "./types"
|