@pyreon/zero 0.12.6 → 0.12.8
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/fs-router-Dil4IKZR.js +290 -0
- package/lib/fs-router-Dil4IKZR.js.map +1 -0
- package/lib/image-plugin.js +68 -1
- package/lib/image-plugin.js.map +1 -1
- package/lib/server.js +1337 -265
- package/lib/server.js.map +1 -1
- package/lib/types/image-plugin.d.ts +49 -1
- package/lib/types/image-plugin.d.ts.map +1 -1
- package/lib/types/image.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/server.d.ts +425 -1
- package/lib/types/server.d.ts.map +1 -1
- package/package.json +10 -10
- package/src/image-plugin.ts +143 -0
- package/src/image-types.d.ts +51 -0
- package/src/server.ts +9 -1
package/src/image-plugin.ts
CHANGED
|
@@ -29,6 +29,46 @@ function warnSharpMissing() {
|
|
|
29
29
|
// import { Image } from "@pyreon/zero/image"
|
|
30
30
|
// <Image src="/hero.jpg" width={1920} height={1080} optimize />
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* CDN provider — rewrites image URLs to CDN endpoints.
|
|
34
|
+
* Return the rewritten URL, or null to use local processing.
|
|
35
|
+
*/
|
|
36
|
+
export type ImageCdnProvider = (src: string, opts: {
|
|
37
|
+
width: number
|
|
38
|
+
quality: number
|
|
39
|
+
format: ImageFormat
|
|
40
|
+
}) => string | null
|
|
41
|
+
|
|
42
|
+
/** Built-in CDN providers. */
|
|
43
|
+
export const cdnProviders = {
|
|
44
|
+
/** Cloudinary: `https://res.cloudinary.com/{cloud}/image/upload/...` */
|
|
45
|
+
cloudinary: (cloudName: string): ImageCdnProvider => (src, { width, quality, format }) =>
|
|
46
|
+
`https://res.cloudinary.com/${cloudName}/image/upload/w_${width},q_${quality},f_${format}/${src}`,
|
|
47
|
+
|
|
48
|
+
/** Imgix: `https://{domain}.imgix.net/...?w=...&q=...&fm=...` */
|
|
49
|
+
imgix: (domain: string): ImageCdnProvider => (src, { width, quality, format }) =>
|
|
50
|
+
`https://${domain}.imgix.net/${src}?w=${width}&q=${quality}&fm=${format}&auto=format`,
|
|
51
|
+
|
|
52
|
+
/** Vercel Image Optimization: `/_next/image?url=...&w=...&q=...` */
|
|
53
|
+
vercel: (): ImageCdnProvider => (src, { width, quality }) =>
|
|
54
|
+
`/_vercel/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`,
|
|
55
|
+
|
|
56
|
+
/** Bunny CDN: `https://{pullZone}.b-cdn.net/...?width=...&quality=...` */
|
|
57
|
+
bunny: (pullZone: string): ImageCdnProvider => (src, { width, quality }) =>
|
|
58
|
+
`https://${pullZone}.b-cdn.net/${src}?width=${width}&quality=${quality}`,
|
|
59
|
+
} as const
|
|
60
|
+
|
|
61
|
+
/** Placeholder generation strategy. */
|
|
62
|
+
export type PlaceholderStrategy = 'blur' | 'dominant-color' | 'none'
|
|
63
|
+
|
|
64
|
+
/** SVG processing options for ?component imports. */
|
|
65
|
+
export interface SvgOptions {
|
|
66
|
+
/** Replace fill/stroke with currentColor. Default: true */
|
|
67
|
+
currentColor?: boolean
|
|
68
|
+
/** Default size (width/height). */
|
|
69
|
+
defaultSize?: number
|
|
70
|
+
}
|
|
71
|
+
|
|
32
72
|
export interface ImagePluginConfig {
|
|
33
73
|
/** Output directory for processed images. Default: "assets/img" */
|
|
34
74
|
outDir?: string
|
|
@@ -40,8 +80,31 @@ export interface ImagePluginConfig {
|
|
|
40
80
|
quality?: number
|
|
41
81
|
/** Blur placeholder size in px. Default: 16 */
|
|
42
82
|
placeholderSize?: number
|
|
83
|
+
/** Placeholder strategy. Default: "blur" */
|
|
84
|
+
placeholder?: PlaceholderStrategy
|
|
43
85
|
/** File patterns to process. Default: /\.(jpe?g|png|webp|avif)$/i */
|
|
44
86
|
include?: RegExp
|
|
87
|
+
/**
|
|
88
|
+
* CDN provider for URL rewriting. When set, images are NOT processed
|
|
89
|
+
* locally — URLs are rewritten to the CDN endpoint.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* imagePlugin({ cdn: cdnProviders.cloudinary('my-cloud') })
|
|
94
|
+
* imagePlugin({ cdn: cdnProviders.vercel() })
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
cdn?: ImageCdnProvider
|
|
98
|
+
/**
|
|
99
|
+
* SVG processing options. Enables `?component` import for inline SVGs.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```tsx
|
|
103
|
+
* import Logo from './logo.svg?component'
|
|
104
|
+
* <Logo width={24} class="text-primary" />
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
svg?: SvgOptions | boolean
|
|
45
108
|
}
|
|
46
109
|
|
|
47
110
|
export type ImageFormat = 'webp' | 'avif' | 'jpeg' | 'png'
|
|
@@ -102,8 +165,15 @@ export function imagePlugin(config: ImagePluginConfig = {}): Plugin {
|
|
|
102
165
|
const defaultFormats = config.formats ?? ['webp']
|
|
103
166
|
const quality = config.quality ?? 80
|
|
104
167
|
const placeholderSize = config.placeholderSize ?? 16
|
|
168
|
+
const placeholderStrategy = config.placeholder ?? 'blur'
|
|
105
169
|
const outSubDir = config.outDir ?? 'assets/img'
|
|
106
170
|
const include = config.include ?? IMAGE_EXT_RE
|
|
171
|
+
const cdn = config.cdn
|
|
172
|
+
const svgOpts: SvgOptions | false = config.svg === true
|
|
173
|
+
? { currentColor: true }
|
|
174
|
+
: config.svg === false || config.svg === undefined
|
|
175
|
+
? false
|
|
176
|
+
: config.svg
|
|
107
177
|
|
|
108
178
|
let root = ''
|
|
109
179
|
let outDir = ''
|
|
@@ -120,6 +190,10 @@ export function imagePlugin(config: ImagePluginConfig = {}): Plugin {
|
|
|
120
190
|
},
|
|
121
191
|
|
|
122
192
|
async resolveId(id) {
|
|
193
|
+
// SVG as component: import Logo from './logo.svg?component'
|
|
194
|
+
if (svgOpts && id.includes('?component') && id.split('?')[0]!.endsWith('.svg')) {
|
|
195
|
+
return `\0virtual:zero-svg:${id}`
|
|
196
|
+
}
|
|
123
197
|
// Handle ?optimize query on image imports
|
|
124
198
|
if (id.includes('?optimize') && include.test(id.split('?')[0]!)) {
|
|
125
199
|
return `\0virtual:zero-image:${id}`
|
|
@@ -128,11 +202,80 @@ export function imagePlugin(config: ImagePluginConfig = {}): Plugin {
|
|
|
128
202
|
},
|
|
129
203
|
|
|
130
204
|
async load(id) {
|
|
205
|
+
// SVG component loading
|
|
206
|
+
if (id.startsWith('\0virtual:zero-svg:')) {
|
|
207
|
+
const rawPath = id.replace('\0virtual:zero-svg:', '').split('?')[0] ?? id
|
|
208
|
+
const absPath = rawPath.startsWith('/') ? join(root, rawPath) : rawPath
|
|
209
|
+
if (!existsSync(absPath)) return null
|
|
210
|
+
|
|
211
|
+
let svg = await readFile(absPath, 'utf-8')
|
|
212
|
+
|
|
213
|
+
// Replace fill/stroke with currentColor
|
|
214
|
+
if (svgOpts && (svgOpts as SvgOptions).currentColor !== false) {
|
|
215
|
+
svg = svg
|
|
216
|
+
.replace(/fill="(?!none)[^"]*"/g, 'fill="currentColor"')
|
|
217
|
+
.replace(/stroke="(?!none)[^"]*"/g, 'stroke="currentColor"')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Add default size from config
|
|
221
|
+
const defaultSize = svgOpts && (svgOpts as SvgOptions).defaultSize
|
|
222
|
+
if (defaultSize && !svg.includes('width=')) {
|
|
223
|
+
svg = svg.replace('<svg', `<svg width="${defaultSize}" height="${defaultSize}"`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Export as Pyreon component
|
|
227
|
+
return `
|
|
228
|
+
import { h } from '@pyreon/core'
|
|
229
|
+
const _svg = ${JSON.stringify(svg)}
|
|
230
|
+
export default function SvgComponent(props) {
|
|
231
|
+
const el = h('span', {
|
|
232
|
+
...props,
|
|
233
|
+
dangerouslySetInnerHTML: { __html: _svg },
|
|
234
|
+
style: [
|
|
235
|
+
'display:inline-flex;align-items:center;justify-content:center',
|
|
236
|
+
props.width ? 'width:' + props.width + 'px' : '',
|
|
237
|
+
props.height ? 'height:' + props.height + 'px' : '',
|
|
238
|
+
props.style || '',
|
|
239
|
+
].filter(Boolean).join(';'),
|
|
240
|
+
})
|
|
241
|
+
return el
|
|
242
|
+
}
|
|
243
|
+
`
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Image optimization loading
|
|
131
247
|
if (!id.startsWith('\0virtual:zero-image:')) return null
|
|
132
248
|
|
|
133
249
|
const rawPath = id.replace('\0virtual:zero-image:', '').split('?')[0] ?? id
|
|
134
250
|
const absPath = rawPath.startsWith('/') ? join(root, 'public', rawPath) : rawPath
|
|
135
251
|
|
|
252
|
+
// CDN mode — rewrite URLs, no local processing
|
|
253
|
+
if (cdn) {
|
|
254
|
+
const metadata = await getImageMetadata(absPath)
|
|
255
|
+
const sources = defaultWidths.map((w) => ({
|
|
256
|
+
src: cdn(rawPath, { width: w, quality, format: defaultFormats[0]! }) ?? rawPath,
|
|
257
|
+
width: w,
|
|
258
|
+
format: defaultFormats[0]! as string,
|
|
259
|
+
}))
|
|
260
|
+
const srcset = sources.map((s) => `${s.src} ${s.width}w`).join(', ')
|
|
261
|
+
const result: ProcessedImage = {
|
|
262
|
+
src: sources[sources.length - 1]?.src ?? rawPath,
|
|
263
|
+
srcset,
|
|
264
|
+
width: metadata.width,
|
|
265
|
+
height: metadata.height,
|
|
266
|
+
placeholder: placeholderStrategy === 'none' ? ''
|
|
267
|
+
: await generateBlurPlaceholder(absPath, placeholderSize),
|
|
268
|
+
formats: defaultFormats.map((fmt) => ({
|
|
269
|
+
type: `image/${fmt}`,
|
|
270
|
+
srcset: defaultWidths
|
|
271
|
+
.map((w) => `${cdn(rawPath, { width: w, quality, format: fmt }) ?? rawPath} ${w}w`)
|
|
272
|
+
.join(', '),
|
|
273
|
+
})),
|
|
274
|
+
sources,
|
|
275
|
+
}
|
|
276
|
+
return `export default ${JSON.stringify(result)}`
|
|
277
|
+
}
|
|
278
|
+
|
|
136
279
|
if (!isBuild) {
|
|
137
280
|
const result = await loadDevImage(absPath, rawPath, placeholderSize)
|
|
138
281
|
return `export default ${JSON.stringify(result)}`
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for image imports processed by @pyreon/zero's imagePlugin.
|
|
3
|
+
*
|
|
4
|
+
* Add to your tsconfig.json:
|
|
5
|
+
* "types": ["@pyreon/zero/image-types"]
|
|
6
|
+
*
|
|
7
|
+
* Or reference directly:
|
|
8
|
+
* /// <reference types="@pyreon/zero/image-types" />
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
declare module '*.jpg?optimize' {
|
|
12
|
+
const image: import('./image-plugin').ProcessedImage
|
|
13
|
+
export default image
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module '*.jpeg?optimize' {
|
|
17
|
+
const image: import('./image-plugin').ProcessedImage
|
|
18
|
+
export default image
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare module '*.png?optimize' {
|
|
22
|
+
const image: import('./image-plugin').ProcessedImage
|
|
23
|
+
export default image
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare module '*.webp?optimize' {
|
|
27
|
+
const image: import('./image-plugin').ProcessedImage
|
|
28
|
+
export default image
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare module '*.avif?optimize' {
|
|
32
|
+
const image: import('./image-plugin').ProcessedImage
|
|
33
|
+
export default image
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare module '*.svg?component' {
|
|
37
|
+
import type { ComponentFn } from '@pyreon/core'
|
|
38
|
+
const component: ComponentFn<{
|
|
39
|
+
width?: number
|
|
40
|
+
height?: number
|
|
41
|
+
class?: string
|
|
42
|
+
style?: string
|
|
43
|
+
[key: string]: unknown
|
|
44
|
+
}>
|
|
45
|
+
export default component
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
declare module '*.svg?raw' {
|
|
49
|
+
const svg: string
|
|
50
|
+
export default svg
|
|
51
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -57,9 +57,17 @@ export { render404Page } from "./not-found";
|
|
|
57
57
|
|
|
58
58
|
export { compose, getContext } from "./middleware";
|
|
59
59
|
|
|
60
|
-
// ─── Vite
|
|
60
|
+
// ─── Vite plugins ───────────────────────────────────────────────────────────
|
|
61
61
|
|
|
62
62
|
export { zeroPlugin as default } from "./vite-plugin";
|
|
63
|
+
export type { FaviconPluginConfig, FaviconLocaleConfig } from "./favicon";
|
|
64
|
+
export { faviconPlugin, faviconLinks } from "./favicon";
|
|
65
|
+
export type { SeoPluginConfig, SitemapConfig, RobotsConfig } from "./seo";
|
|
66
|
+
export { seoPlugin, generateSitemap, generateRobots, jsonLd, seoMiddleware } from "./seo";
|
|
67
|
+
export type { OgImagePluginConfig, OgImageTemplate, OgImageLayer } from "./og-image";
|
|
68
|
+
export { ogImagePlugin, ogImagePath } from "./og-image";
|
|
69
|
+
export type { AiPluginConfig, InferJsonLdOptions } from "./ai";
|
|
70
|
+
export { aiPlugin, inferJsonLd, generateLlmsTxt, generateLlmsFullTxt } from "./ai";
|
|
63
71
|
|
|
64
72
|
// ─── I18n server-only ───────────────────────────────────────────────────────
|
|
65
73
|
|