@pyreon/zero 0.12.3 → 0.12.5
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/README.md +15 -2
- package/lib/favicon.js +151 -6
- package/lib/favicon.js.map +1 -1
- package/lib/index.js +343 -4039
- package/lib/index.js.map +1 -1
- package/lib/meta.js +22 -48
- package/lib/meta.js.map +1 -1
- package/lib/server.js +1534 -0
- package/lib/server.js.map +1 -0
- package/lib/theme.js +5 -2
- package/lib/theme.js.map +1 -1
- package/lib/types/favicon.d.ts +17 -0
- package/lib/types/favicon.d.ts.map +1 -1
- package/lib/types/index.d.ts +189 -1540
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/meta.d.ts +11 -46
- package/lib/types/meta.d.ts.map +1 -1
- package/lib/types/server.d.ts +455 -0
- package/lib/types/server.d.ts.map +1 -0
- package/lib/types/theme.d.ts +1 -1
- package/lib/types/theme.d.ts.map +1 -1
- package/package.json +15 -10
- package/src/favicon.ts +163 -33
- package/src/index.ts +47 -182
- package/src/meta.tsx +37 -3
- package/src/server.ts +70 -0
- package/src/theme.tsx +10 -3
- package/lib/fs-router-Dil4IKZR.js +0 -290
- package/lib/fs-router-Dil4IKZR.js.map +0 -1
package/README.md
CHANGED
|
@@ -21,9 +21,18 @@ bun add @pyreon/zero
|
|
|
21
21
|
- **Image optimization** — Build-time processing via `?optimize` imports (WebP/AVIF, blur placeholders)
|
|
22
22
|
- **SEO** — Sitemap, robots.txt, JSON-LD helpers (Vite plugin + dev middleware)
|
|
23
23
|
- **Middleware** — `cacheMiddleware()`, `securityHeaders()`, `corsMiddleware()`, `rateLimitMiddleware()`, `compressionMiddleware()`
|
|
24
|
-
- **Adapters** — Node.js, Bun, static
|
|
24
|
+
- **Adapters** — Node.js, Bun, static, Vercel, Cloudflare Pages, Netlify Functions
|
|
25
25
|
- **Testing** — `createTestContext()`, `testMiddleware()`, `createTestApiServer()`, `createMockHandler()`
|
|
26
26
|
- **Dev overlay** — Styled error overlay with source-mapped stack traces for SSR errors
|
|
27
|
+
- **CSP middleware** — `cspMiddleware({ directives })` with `useNonce()` for inline scripts
|
|
28
|
+
- **Env validation** — `validateEnv()` with type coercion, `schema()` for custom parsers, `publicEnv()`
|
|
29
|
+
- **Request logging** — `loggerMiddleware()` with structured output
|
|
30
|
+
- **AI integration** — `aiPlugin()` for llms.txt, JSON-LD inference, AI plugin manifest
|
|
31
|
+
- **useRequestLocals()** — Bridge middleware locals into components
|
|
32
|
+
- **Locale-aware favicons** — Per-locale favicon generation from source SVG/PNG
|
|
33
|
+
- **OG image generation** — Build-time Open Graph image rendering
|
|
34
|
+
- **Reactive favicon** — Theme-synced light/dark favicon switching
|
|
35
|
+
- **Client-safe entry** — `@pyreon/zero` = client-safe, `@pyreon/zero/server` = server-only
|
|
27
36
|
|
|
28
37
|
## Usage
|
|
29
38
|
|
|
@@ -41,7 +50,7 @@ export default {
|
|
|
41
50
|
|
|
42
51
|
| Export | Description |
|
|
43
52
|
| --------------------------- | ----------------------------------------------------- |
|
|
44
|
-
| `@pyreon/zero` |
|
|
53
|
+
| `@pyreon/zero` | Client-safe: components, middleware, adapters, theme, SEO, fonts |
|
|
45
54
|
| `@pyreon/zero/client` | Client-side entry (`startClient`) |
|
|
46
55
|
| `@pyreon/zero/config` | `defineConfig`, `resolveConfig` |
|
|
47
56
|
| `@pyreon/zero/image` | `<Image>` component |
|
|
@@ -58,6 +67,10 @@ export default {
|
|
|
58
67
|
| `@pyreon/zero/rate-limit` | Rate limiting middleware |
|
|
59
68
|
| `@pyreon/zero/compression` | Compression middleware |
|
|
60
69
|
| `@pyreon/zero/testing` | Test utilities for middleware and API routes |
|
|
70
|
+
| `@pyreon/zero/server` | Server-only: `createServer`, `validateEnv`, `useNonce`, `useRequestLocals` |
|
|
71
|
+
| `@pyreon/zero/adapter-vercel` | Vercel serverless deployment adapter |
|
|
72
|
+
| `@pyreon/zero/adapter-cloudflare` | Cloudflare Pages deployment adapter |
|
|
73
|
+
| `@pyreon/zero/adapter-netlify` | Netlify Functions deployment adapter |
|
|
61
74
|
|
|
62
75
|
## License
|
|
63
76
|
|
package/lib/favicon.js
CHANGED
|
@@ -62,7 +62,16 @@ function faviconPlugin(config) {
|
|
|
62
62
|
},
|
|
63
63
|
configureServer(server) {
|
|
64
64
|
const sourcePath = join(root, config.source);
|
|
65
|
+
const darkPath = config.darkSource ? join(root, config.darkSource) : null;
|
|
66
|
+
const devSourcePath = typeof config.devSource === "string" ? join(root, config.devSource) : null;
|
|
67
|
+
const autoDevBadge = config.devSource === true;
|
|
65
68
|
const devCache = /* @__PURE__ */ new Map();
|
|
69
|
+
/** Resolve source path for a request — handles dark variants and dev badge. */
|
|
70
|
+
function resolveSourceForDev(baseName, defaultSource) {
|
|
71
|
+
if (darkPath && baseName.includes("-dark-")) return darkPath;
|
|
72
|
+
if (baseName.includes("-light-")) return defaultSource;
|
|
73
|
+
return defaultSource;
|
|
74
|
+
}
|
|
66
75
|
server.middlewares.use(async (req, res, next) => {
|
|
67
76
|
const url = req.url ?? "";
|
|
68
77
|
const localeSource = resolveLocaleSource(url, config, root);
|
|
@@ -70,18 +79,23 @@ function faviconPlugin(config) {
|
|
|
70
79
|
const svgPath = localeSource ? localeSource.sourcePath : sourcePath;
|
|
71
80
|
const isSvgSource = localeSource ? localeSource.source.endsWith(".svg") : config.source.endsWith(".svg");
|
|
72
81
|
if (svgUrl.endsWith("/favicon.svg") && isSvgSource) try {
|
|
73
|
-
|
|
82
|
+
let content = await readFile(svgPath, "utf-8");
|
|
83
|
+
if (autoDevBadge) content = addDevBadgeToSvg(content);
|
|
84
|
+
else if (devSourcePath && existsSync(devSourcePath)) content = await readFile(devSourcePath, "utf-8");
|
|
74
85
|
res.setHeader("Content-Type", "image/svg+xml");
|
|
75
86
|
res.end(content);
|
|
76
87
|
return;
|
|
77
88
|
} catch {}
|
|
78
89
|
const baseName = svgUrl.split("/").pop() ?? "";
|
|
79
|
-
const
|
|
90
|
+
const cleanName = baseName.replace(/-?(light|dark)-/, "-");
|
|
91
|
+
const sizeMatch = SIZES.find((s) => s.name === cleanName || baseName === s.name);
|
|
80
92
|
if (sizeMatch) {
|
|
81
|
-
const
|
|
93
|
+
const resolvedSource = resolveSourceForDev(baseName, svgPath);
|
|
94
|
+
const cacheKey = `${resolvedSource}:${sizeMatch.size}:${autoDevBadge}`;
|
|
82
95
|
let png = devCache.get(cacheKey);
|
|
83
96
|
if (!png) {
|
|
84
|
-
|
|
97
|
+
let result = await resizeToPng(resolvedSource, sizeMatch.size);
|
|
98
|
+
if (result && autoDevBadge) result = await addDevBadgeToPng(result, sizeMatch.size);
|
|
85
99
|
if (result) {
|
|
86
100
|
png = result;
|
|
87
101
|
devCache.set(cacheKey, result);
|
|
@@ -138,6 +152,7 @@ function faviconPlugin(config) {
|
|
|
138
152
|
},
|
|
139
153
|
transformIndexHtml() {
|
|
140
154
|
const isSvg = config.source.endsWith(".svg");
|
|
155
|
+
const hasDark = !!config.darkSource;
|
|
141
156
|
const tags = [];
|
|
142
157
|
if (isSvg) tags.push({
|
|
143
158
|
tag: "link",
|
|
@@ -148,7 +163,72 @@ function faviconPlugin(config) {
|
|
|
148
163
|
},
|
|
149
164
|
injectTo: "head"
|
|
150
165
|
});
|
|
151
|
-
|
|
166
|
+
if (hasDark) {
|
|
167
|
+
const lightAttrs = { "data-favicon-theme": "light" };
|
|
168
|
+
const darkAttrs = {
|
|
169
|
+
"data-favicon-theme": "dark",
|
|
170
|
+
media: "not all"
|
|
171
|
+
};
|
|
172
|
+
tags.push({
|
|
173
|
+
tag: "link",
|
|
174
|
+
attrs: {
|
|
175
|
+
rel: "icon",
|
|
176
|
+
type: "image/png",
|
|
177
|
+
sizes: "32x32",
|
|
178
|
+
href: "/favicon-light-32x32.png",
|
|
179
|
+
...lightAttrs
|
|
180
|
+
},
|
|
181
|
+
injectTo: "head"
|
|
182
|
+
}, {
|
|
183
|
+
tag: "link",
|
|
184
|
+
attrs: {
|
|
185
|
+
rel: "icon",
|
|
186
|
+
type: "image/png",
|
|
187
|
+
sizes: "32x32",
|
|
188
|
+
href: "/favicon-dark-32x32.png",
|
|
189
|
+
...darkAttrs
|
|
190
|
+
},
|
|
191
|
+
injectTo: "head"
|
|
192
|
+
}, {
|
|
193
|
+
tag: "link",
|
|
194
|
+
attrs: {
|
|
195
|
+
rel: "icon",
|
|
196
|
+
type: "image/png",
|
|
197
|
+
sizes: "16x16",
|
|
198
|
+
href: "/favicon-light-16x16.png",
|
|
199
|
+
...lightAttrs
|
|
200
|
+
},
|
|
201
|
+
injectTo: "head"
|
|
202
|
+
}, {
|
|
203
|
+
tag: "link",
|
|
204
|
+
attrs: {
|
|
205
|
+
rel: "icon",
|
|
206
|
+
type: "image/png",
|
|
207
|
+
sizes: "16x16",
|
|
208
|
+
href: "/favicon-dark-16x16.png",
|
|
209
|
+
...darkAttrs
|
|
210
|
+
},
|
|
211
|
+
injectTo: "head"
|
|
212
|
+
}, {
|
|
213
|
+
tag: "link",
|
|
214
|
+
attrs: {
|
|
215
|
+
rel: "apple-touch-icon",
|
|
216
|
+
sizes: "180x180",
|
|
217
|
+
href: "/apple-touch-icon-light.png",
|
|
218
|
+
...lightAttrs
|
|
219
|
+
},
|
|
220
|
+
injectTo: "head"
|
|
221
|
+
}, {
|
|
222
|
+
tag: "link",
|
|
223
|
+
attrs: {
|
|
224
|
+
rel: "apple-touch-icon",
|
|
225
|
+
sizes: "180x180",
|
|
226
|
+
href: "/apple-touch-icon-dark.png",
|
|
227
|
+
...darkAttrs
|
|
228
|
+
},
|
|
229
|
+
injectTo: "head"
|
|
230
|
+
});
|
|
231
|
+
} else tags.push({
|
|
152
232
|
tag: "link",
|
|
153
233
|
attrs: {
|
|
154
234
|
rel: "icon",
|
|
@@ -257,7 +337,36 @@ async function generateFaviconSet(rootDir, source, darkSource, prefix, config, t
|
|
|
257
337
|
source: finalSvg
|
|
258
338
|
});
|
|
259
339
|
}
|
|
260
|
-
|
|
340
|
+
if (darkSource) {
|
|
341
|
+
const darkPath = join(rootDir, darkSource);
|
|
342
|
+
const darkExists = existsSync(darkPath);
|
|
343
|
+
for (const { size, name } of SIZES) {
|
|
344
|
+
const lightName = name.replace(/^(favicon-)/, "$1light-").replace(/^(apple-touch-icon)/, "$1-light").replace(/^(icon-)/, "$1light-");
|
|
345
|
+
const lightPng = await resizeToPng(sourcePath, size);
|
|
346
|
+
if (lightPng) this.emitFile({
|
|
347
|
+
type: "asset",
|
|
348
|
+
fileName: `${prefix}${lightName}`,
|
|
349
|
+
source: lightPng
|
|
350
|
+
});
|
|
351
|
+
if (darkExists) {
|
|
352
|
+
const darkName = name.replace(/^(favicon-)/, "$1dark-").replace(/^(apple-touch-icon)/, "$1-dark").replace(/^(icon-)/, "$1dark-");
|
|
353
|
+
const darkPng = await resizeToPng(darkPath, size);
|
|
354
|
+
if (darkPng) this.emitFile({
|
|
355
|
+
type: "asset",
|
|
356
|
+
fileName: `${prefix}${darkName}`,
|
|
357
|
+
source: darkPng
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (const { size, name } of SIZES) {
|
|
362
|
+
const pngBuffer = await resizeToPng(sourcePath, size);
|
|
363
|
+
if (pngBuffer) this.emitFile({
|
|
364
|
+
type: "asset",
|
|
365
|
+
fileName: `${prefix}${name}`,
|
|
366
|
+
source: pngBuffer
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
} else for (const { size, name } of SIZES) {
|
|
261
370
|
const pngBuffer = await resizeToPng(sourcePath, size);
|
|
262
371
|
if (pngBuffer) this.emitFile({
|
|
263
372
|
type: "asset",
|
|
@@ -418,6 +527,42 @@ function createIcoFromPngs(entries) {
|
|
|
418
527
|
...dataBuffers
|
|
419
528
|
]);
|
|
420
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* Add a "DEV" badge overlay to an SVG string.
|
|
532
|
+
* Adds a small colored circle with "DEV" text in the bottom-right corner.
|
|
533
|
+
*/
|
|
534
|
+
function addDevBadgeToSvg(svg) {
|
|
535
|
+
const [, , w, h] = (svg.match(/viewBox="([^"]*)"/)?.[1] ?? "0 0 32 32").split(" ").map(Number);
|
|
536
|
+
const size = Math.min(w ?? 32, h ?? 32);
|
|
537
|
+
const r = size * .28;
|
|
538
|
+
const cx = (w ?? 32) - r;
|
|
539
|
+
const cy = (h ?? 32) - r;
|
|
540
|
+
const fontSize = r * .85;
|
|
541
|
+
const badge = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="#ef4444" stroke="white" stroke-width="${size * .03}"/><text x="${cx}" y="${cy}" font-size="${fontSize}" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="central" font-family="sans-serif">D</text>`;
|
|
542
|
+
return svg.replace(/<\/svg>\s*$/, `${badge}</svg>`);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Add a "DEV" badge to a PNG buffer via sharp composite.
|
|
546
|
+
* Composites a red circle with "D" in the bottom-right corner.
|
|
547
|
+
*/
|
|
548
|
+
async function addDevBadgeToPng(pngBuffer, size) {
|
|
549
|
+
try {
|
|
550
|
+
const sharp = await import("sharp").then((m) => m.default ?? m);
|
|
551
|
+
const r = Math.round(size * .28);
|
|
552
|
+
const d = r * 2;
|
|
553
|
+
const badgeSvg = `<svg width="${d}" height="${d}" xmlns="http://www.w3.org/2000/svg">
|
|
554
|
+
<circle cx="${r}" cy="${r}" r="${r}" fill="#ef4444"/>
|
|
555
|
+
<text x="${r}" y="${r}" font-size="${Math.round(r * .85)}" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="central" font-family="sans-serif">D</text>
|
|
556
|
+
</svg>`;
|
|
557
|
+
const badgePng = await sharp(Buffer.from(badgeSvg)).png().toBuffer();
|
|
558
|
+
return await sharp(Buffer.from(pngBuffer)).composite([{
|
|
559
|
+
input: badgePng,
|
|
560
|
+
gravity: "southeast"
|
|
561
|
+
}]).png().toBuffer();
|
|
562
|
+
} catch {
|
|
563
|
+
return pngBuffer;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
421
566
|
|
|
422
567
|
//#endregion
|
|
423
568
|
export { createIcoFromPngs, faviconLinks, faviconPlugin };
|
package/lib/favicon.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"favicon.js","names":[],"sources":["../src/favicon.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[zero:favicon] sharp not installed — favicons will not be generated. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Favicon generation plugin ──────────────────────────────────────────────\n//\n// Generates all favicon formats from a single source file (SVG or PNG):\n// - favicon.ico (16x16 + 32x32 combined)\n// - favicon.svg (copied if source is SVG)\n// - apple-touch-icon.png (180x180)\n// - icon-192.png (for web manifest)\n// - icon-512.png (for web manifest)\n// - site.webmanifest\n//\n// Usage:\n// import { faviconPlugin } from \"@pyreon/zero\"\n// export default { plugins: [zero(), faviconPlugin({ source: \"./icon.svg\" })] }\n\nexport interface FaviconLocaleConfig {\n /** Locale-specific source icon (SVG or PNG). */\n source: string\n /** Optional dark mode variant for this locale. */\n darkSource?: string\n}\n\nexport interface FaviconPluginConfig {\n /** Path to the source icon (SVG or PNG, at least 512x512 for PNG). */\n source: string\n /** Theme color for web manifest. Default: \"#ffffff\" */\n themeColor?: string\n /** Background color for web manifest. Default: \"#ffffff\" */\n backgroundColor?: string\n /** App name for web manifest. Uses package.json name if not set. */\n name?: string\n /** Generate web manifest. Default: true */\n manifest?: boolean\n /**\n * Dark mode favicon (SVG only).\n * When provided, the SVG favicon uses prefers-color-scheme media query\n * to switch between light and dark variants.\n */\n darkSource?: string\n /**\n * Locale-specific icon overrides. Each key is a locale code,\n * value is a source icon (and optional dark variant).\n * Locales not in this map use the base `source`.\n *\n * Generated files are placed under `/{locale}/` prefix:\n * /de/favicon.svg, /de/favicon-32x32.png, etc.\n *\n * @example\n * ```ts\n * faviconPlugin({\n * source: \"./icon.svg\",\n * locales: {\n * de: { source: \"./icon-de.svg\" },\n * cs: { source: \"./icon-cs.svg\" },\n * },\n * })\n * ```\n */\n locales?: Record<string, FaviconLocaleConfig>\n}\n\ninterface FaviconSize {\n size: number\n name: string\n}\n\nconst SIZES: FaviconSize[] = [\n { size: 16, name: 'favicon-16x16.png' },\n { size: 32, name: 'favicon-32x32.png' },\n { size: 180, name: 'apple-touch-icon.png' },\n { size: 192, name: 'icon-192.png' },\n { size: 512, name: 'icon-512.png' },\n]\n\n/**\n * Favicon generation Vite plugin.\n *\n * Generates all required favicon formats at build time from a single source.\n * In dev mode, serves the source directly.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { faviconPlugin } from \"@pyreon/zero\"\n *\n * export default {\n * plugins: [faviconPlugin({ source: \"./src/assets/icon.svg\" })],\n * }\n * ```\n */\nexport function faviconPlugin(config: FaviconPluginConfig): Plugin {\n const themeColor = config.themeColor ?? '#ffffff'\n const backgroundColor = config.backgroundColor ?? '#ffffff'\n const generateManifest = config.manifest !== false\n\n let root = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-favicon',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n isBuild = resolvedConfig.command === 'build'\n },\n\n // Dev server: serve generated favicons on-the-fly\n configureServer(server) {\n const sourcePath = join(root, config.source)\n const devCache = new Map<string, Uint8Array>()\n\n server.middlewares.use(async (req, res, next) => {\n const url = req.url ?? ''\n\n // Resolve locale-specific source: /{locale}/favicon.svg → locale source\n const localeSource = resolveLocaleSource(url, config, root)\n\n // Serve source as favicon.svg in dev\n const svgUrl = localeSource ? localeSource.url : url\n const svgPath = localeSource ? localeSource.sourcePath : sourcePath\n const isSvgSource = localeSource ? localeSource.source.endsWith('.svg') : config.source.endsWith('.svg')\n\n if (svgUrl.endsWith('/favicon.svg') && isSvgSource) {\n try {\n const content = await readFile(svgPath, 'utf-8')\n res.setHeader('Content-Type', 'image/svg+xml')\n res.end(content)\n return\n } catch { /* fall through */ }\n }\n\n // Serve generated PNGs on-demand (supports /{locale}/favicon-32x32.png)\n const baseName = svgUrl.split('/').pop() ?? ''\n const sizeMatch = SIZES.find((s) => s.name === baseName)\n if (sizeMatch) {\n const cacheKey = `${svgPath}:${sizeMatch.size}`\n let png = devCache.get(cacheKey)\n if (!png) {\n const result = await resizeToPng(svgPath, sizeMatch.size)\n if (result) {\n png = result\n devCache.set(cacheKey, result)\n }\n }\n if (png) {\n res.setHeader('Content-Type', 'image/png')\n res.setHeader('Cache-Control', 'no-cache')\n res.end(Buffer.from(png))\n return\n }\n }\n\n // Serve generated ICO on-demand\n if (baseName === 'favicon.ico') {\n const cacheKey = `ico:${svgPath}`\n let ico: Uint8Array | undefined = devCache.get(cacheKey)\n if (!ico) {\n const result = await generateIco(svgPath)\n if (result) {\n ico = result\n devCache.set(cacheKey, result)\n }\n }\n if (ico) {\n res.setHeader('Content-Type', 'image/x-icon')\n res.setHeader('Cache-Control', 'no-cache')\n res.end(Buffer.from(ico))\n return\n }\n }\n\n // Serve manifest (supports /{locale}/site.webmanifest)\n if (baseName === 'site.webmanifest' && generateManifest) {\n const prefix = localeSource ? `/${localeSource.locale}` : ''\n const manifest = {\n name: config.name ?? 'App',\n short_name: config.name ?? 'App',\n icons: [\n { src: `${prefix}/icon-192.png`, sizes: '192x192', type: 'image/png' },\n { src: `${prefix}/icon-512.png`, sizes: '512x512', type: 'image/png' },\n ],\n theme_color: themeColor,\n background_color: backgroundColor,\n display: 'standalone',\n }\n res.setHeader('Content-Type', 'application/manifest+json')\n res.end(JSON.stringify(manifest, null, 2))\n return\n }\n\n next()\n })\n },\n\n // Inject favicon <link> tags into HTML\n transformIndexHtml() {\n const isSvg = config.source.endsWith('.svg')\n const tags: Array<{\n tag: string\n attrs: Record<string, string>\n injectTo: 'head'\n }> = []\n\n if (isSvg) {\n tags.push({\n tag: 'link',\n attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },\n injectTo: 'head',\n })\n }\n\n tags.push(\n {\n tag: 'link',\n attrs: { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' },\n injectTo: 'head',\n },\n {\n tag: 'link',\n attrs: { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' },\n injectTo: 'head',\n },\n {\n tag: 'link',\n attrs: { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' },\n injectTo: 'head',\n },\n )\n\n if (generateManifest) {\n tags.push({\n tag: 'link',\n attrs: { rel: 'manifest', href: '/site.webmanifest' },\n injectTo: 'head',\n })\n }\n\n tags.push({\n tag: 'meta',\n attrs: { name: 'theme-color', content: themeColor },\n injectTo: 'head',\n })\n\n return tags\n },\n\n async generateBundle() {\n if (!isBuild) return\n\n // Generate favicons for the base (default) source\n await generateFaviconSet.call(this, root, config.source, config.darkSource, '', config, themeColor, backgroundColor, generateManifest)\n\n // Generate locale-specific favicon sets\n if (config.locales) {\n for (const [locale, localeConfig] of Object.entries(config.locales)) {\n await generateFaviconSet.call(this, root, localeConfig.source, localeConfig.darkSource, `${locale}/`, config, themeColor, backgroundColor, generateManifest)\n }\n }\n },\n }\n}\n\n/**\n * Wrap two SVGs into a single SVG that switches based on prefers-color-scheme.\n */\nfunction wrapSvgWithDarkMode(lightSvg: string, darkSvg: string): string {\n // Extract viewBox from light SVG\n const viewBoxMatch = lightSvg.match(/viewBox=\"([^\"]*)\"/)\n const viewBox = viewBoxMatch?.[1] ?? '0 0 32 32'\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"${viewBox}\">\n <style>\n :root { color-scheme: light dark; }\n @media (prefers-color-scheme: dark) { .light { display: none; } }\n @media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) { .dark { display: none; } }\n </style>\n <g class=\"light\">${stripSvgWrapper(lightSvg)}</g>\n <g class=\"dark\">${stripSvgWrapper(darkSvg)}</g>\n</svg>`\n}\n\nfunction stripSvgWrapper(svg: string): string {\n return svg\n .replace(/<svg[^>]*>/, '')\n .replace(/<\\/svg>\\s*$/, '')\n .trim()\n}\n\n/**\n * Resolve the source path for a locale-prefixed favicon URL.\n * Returns null if the URL is not locale-prefixed or locale has no override.\n */\nfunction resolveLocaleSource(\n url: string,\n config: FaviconPluginConfig,\n rootDir: string,\n): { locale: string; url: string; source: string; sourcePath: string } | null {\n if (!config.locales) return null\n\n for (const [locale, localeConfig] of Object.entries(config.locales)) {\n const prefix = `/${locale}/`\n if (url.startsWith(prefix)) {\n return {\n locale,\n url,\n source: localeConfig.source,\n sourcePath: join(rootDir, localeConfig.source),\n }\n }\n }\n return null\n}\n\n/**\n * Generate a complete favicon set (SVG, PNGs, ICO, manifest) with a file prefix.\n * Called once for base (prefix = '') and once per locale (prefix = '{locale}/').\n */\nasync function generateFaviconSet(\n this: any,\n rootDir: string,\n source: string,\n darkSource: string | undefined,\n prefix: string,\n config: FaviconPluginConfig,\n themeColor: string,\n backgroundColor: string,\n generateManifest: boolean,\n): Promise<void> {\n const sourcePath = join(rootDir, source)\n if (!existsSync(sourcePath)) {\n // oxlint-disable-next-line no-console\n console.warn(`[zero:favicon] Source not found: ${sourcePath}`)\n return\n }\n\n const isSvg = source.endsWith('.svg')\n\n // Copy SVG as favicon.svg\n if (isSvg) {\n const svgContent = await readFile(sourcePath, 'utf-8')\n let finalSvg = svgContent\n\n if (darkSource) {\n const darkPath = join(rootDir, darkSource)\n if (existsSync(darkPath)) {\n const darkSvg = await readFile(darkPath, 'utf-8')\n finalSvg = wrapSvgWithDarkMode(svgContent, darkSvg)\n }\n }\n\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}favicon.svg`,\n source: finalSvg,\n })\n }\n\n // Generate PNG sizes via sharp\n for (const { size, name } of SIZES) {\n const pngBuffer = await resizeToPng(sourcePath, size)\n if (pngBuffer) {\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}${name}`,\n source: pngBuffer,\n })\n }\n }\n\n // Generate favicon.ico (16 + 32)\n const ico = await generateIco(sourcePath)\n if (ico) {\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}favicon.ico`,\n source: ico,\n })\n }\n\n // Generate web manifest\n if (generateManifest) {\n const manifestPrefix = prefix ? `/${prefix.slice(0, -1)}` : ''\n const manifest = {\n name: config.name ?? 'App',\n short_name: config.name ?? 'App',\n icons: [\n { src: `${manifestPrefix}/icon-192.png`, sizes: '192x192', type: 'image/png' },\n { src: `${manifestPrefix}/icon-512.png`, sizes: '512x512', type: 'image/png' },\n ],\n theme_color: themeColor,\n background_color: backgroundColor,\n display: 'standalone',\n }\n\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}site.webmanifest`,\n source: JSON.stringify(manifest, null, 2),\n })\n }\n}\n\n/**\n * Get favicon link tags for a specific locale.\n * Returns link objects suitable for `useHead()` or direct HTML injection.\n *\n * @example\n * ```ts\n * const links = faviconLinks(\"de\", { source: \"./icon.svg\", locales: { de: { source: \"./icon-de.svg\" } } })\n * // → [{ rel: \"icon\", type: \"image/svg+xml\", href: \"/de/favicon.svg\" }, ...]\n * ```\n */\nexport function faviconLinks(\n locale: string | undefined,\n config: FaviconPluginConfig,\n): Array<{ rel: string; type?: string; sizes?: string; href: string }> {\n const hasLocaleOverride = locale && config.locales?.[locale]\n const prefix = hasLocaleOverride ? `/${locale}` : ''\n const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')\n\n const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []\n\n if (isSvg) {\n links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })\n }\n\n links.push(\n { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${prefix}/favicon-32x32.png` },\n { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${prefix}/favicon-16x16.png` },\n { rel: 'apple-touch-icon', sizes: '180x180', href: `${prefix}/apple-touch-icon.png` },\n )\n\n if (config.manifest !== false) {\n links.push({ rel: 'manifest', href: `${prefix}/site.webmanifest` })\n }\n\n return links\n}\n\nasync function resizeToPng(input: string, size: number): Promise<Uint8Array | null> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n return await sharp(input).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } } as any).png().toBuffer()\n } catch {\n warnSharpMissing()\n return null\n }\n}\n\nasync function generateIco(input: string): Promise<Uint8Array | null> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const png16 = await sharp(input).resize(16, 16, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } } as any).png().toBuffer()\n const png32 = await sharp(input).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } } as any).png().toBuffer()\n\n // ICO format: header + directory entries + PNG data\n return createIcoFromPngs([\n { buffer: png16, size: 16 },\n { buffer: png32, size: 32 },\n ])\n } catch {\n warnSharpMissing()\n return null\n }\n}\n\nexport interface IcoEntry {\n buffer: Buffer\n size: number\n}\n\n/** @internal Exported for testing */\nexport function createIcoFromPngs(entries: IcoEntry[]): Uint8Array {\n const headerSize = 6\n const dirEntrySize = 16\n const dirSize = dirEntrySize * entries.length\n let dataOffset = headerSize + dirSize\n\n // ICO header\n const header = Buffer.alloc(headerSize)\n header.writeUInt16LE(0, 0) // reserved\n header.writeUInt16LE(1, 2) // type: icon\n header.writeUInt16LE(entries.length, 4) // count\n\n // Directory entries\n const dirEntries = Buffer.alloc(dirSize)\n const dataBuffers: Buffer[] = []\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i]!\n const offset = i * dirEntrySize\n dirEntries.writeUInt8(entry.size === 256 ? 0 : entry.size, offset) // width\n dirEntries.writeUInt8(entry.size === 256 ? 0 : entry.size, offset + 1) // height\n dirEntries.writeUInt8(0, offset + 2) // palette\n dirEntries.writeUInt8(0, offset + 3) // reserved\n dirEntries.writeUInt16LE(1, offset + 4) // color planes\n dirEntries.writeUInt16LE(32, offset + 6) // bits per pixel\n dirEntries.writeUInt32LE(entry.buffer.length, offset + 8) // size\n dirEntries.writeUInt32LE(dataOffset, offset + 12) // offset\n\n dataOffset += entry.buffer.length\n dataBuffers.push(entry.buffer)\n }\n\n return Buffer.concat([header, dirEntries, ...dataBuffers])\n}\n"],"mappings":";;;;;AAKA,IAAI,cAAc;AAClB,SAAS,mBAAmB;AAC1B,KAAI,YAAa;AACjB,eAAc;AAEd,SAAQ,KACN,sHACD;;AAoEH,MAAM,QAAuB;CAC3B;EAAE,MAAM;EAAI,MAAM;EAAqB;CACvC;EAAE,MAAM;EAAI,MAAM;EAAqB;CACvC;EAAE,MAAM;EAAK,MAAM;EAAwB;CAC3C;EAAE,MAAM;EAAK,MAAM;EAAgB;CACnC;EAAE,MAAM;EAAK,MAAM;EAAgB;CACpC;;;;;;;;;;;;;;;;;AAkBD,SAAgB,cAAc,QAAqC;CACjE,MAAM,aAAa,OAAO,cAAc;CACxC,MAAM,kBAAkB,OAAO,mBAAmB;CAClD,MAAM,mBAAmB,OAAO,aAAa;CAE7C,IAAI,OAAO;CACX,IAAI,UAAU;AAEd,QAAO;EACL,MAAM;EACN,SAAS;EAET,eAAe,gBAAgB;AAC7B,UAAO,eAAe;AACtB,aAAU,eAAe,YAAY;;EAIvC,gBAAgB,QAAQ;GACtB,MAAM,aAAa,KAAK,MAAM,OAAO,OAAO;GAC5C,MAAM,2BAAW,IAAI,KAAyB;AAE9C,UAAO,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;IAC/C,MAAM,MAAM,IAAI,OAAO;IAGvB,MAAM,eAAe,oBAAoB,KAAK,QAAQ,KAAK;IAG3D,MAAM,SAAS,eAAe,aAAa,MAAM;IACjD,MAAM,UAAU,eAAe,aAAa,aAAa;IACzD,MAAM,cAAc,eAAe,aAAa,OAAO,SAAS,OAAO,GAAG,OAAO,OAAO,SAAS,OAAO;AAExG,QAAI,OAAO,SAAS,eAAe,IAAI,YACrC,KAAI;KACF,MAAM,UAAU,MAAM,SAAS,SAAS,QAAQ;AAChD,SAAI,UAAU,gBAAgB,gBAAgB;AAC9C,SAAI,IAAI,QAAQ;AAChB;YACM;IAIV,MAAM,WAAW,OAAO,MAAM,IAAI,CAAC,KAAK,IAAI;IAC5C,MAAM,YAAY,MAAM,MAAM,MAAM,EAAE,SAAS,SAAS;AACxD,QAAI,WAAW;KACb,MAAM,WAAW,GAAG,QAAQ,GAAG,UAAU;KACzC,IAAI,MAAM,SAAS,IAAI,SAAS;AAChC,SAAI,CAAC,KAAK;MACR,MAAM,SAAS,MAAM,YAAY,SAAS,UAAU,KAAK;AACzD,UAAI,QAAQ;AACV,aAAM;AACN,gBAAS,IAAI,UAAU,OAAO;;;AAGlC,SAAI,KAAK;AACP,UAAI,UAAU,gBAAgB,YAAY;AAC1C,UAAI,UAAU,iBAAiB,WAAW;AAC1C,UAAI,IAAI,OAAO,KAAK,IAAI,CAAC;AACzB;;;AAKJ,QAAI,aAAa,eAAe;KAC9B,MAAM,WAAW,OAAO;KACxB,IAAI,MAA8B,SAAS,IAAI,SAAS;AACxD,SAAI,CAAC,KAAK;MACR,MAAM,SAAS,MAAM,YAAY,QAAQ;AACzC,UAAI,QAAQ;AACV,aAAM;AACN,gBAAS,IAAI,UAAU,OAAO;;;AAGlC,SAAI,KAAK;AACP,UAAI,UAAU,gBAAgB,eAAe;AAC7C,UAAI,UAAU,iBAAiB,WAAW;AAC1C,UAAI,IAAI,OAAO,KAAK,IAAI,CAAC;AACzB;;;AAKJ,QAAI,aAAa,sBAAsB,kBAAkB;KACvD,MAAM,SAAS,eAAe,IAAI,aAAa,WAAW;KAC1D,MAAM,WAAW;MACf,MAAM,OAAO,QAAQ;MACrB,YAAY,OAAO,QAAQ;MAC3B,OAAO,CACL;OAAE,KAAK,GAAG,OAAO;OAAgB,OAAO;OAAW,MAAM;OAAa,EACtE;OAAE,KAAK,GAAG,OAAO;OAAgB,OAAO;OAAW,MAAM;OAAa,CACvE;MACD,aAAa;MACb,kBAAkB;MAClB,SAAS;MACV;AACD,SAAI,UAAU,gBAAgB,4BAA4B;AAC1D,SAAI,IAAI,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC;AAC1C;;AAGF,UAAM;KACN;;EAIJ,qBAAqB;GACnB,MAAM,QAAQ,OAAO,OAAO,SAAS,OAAO;GAC5C,MAAM,OAID,EAAE;AAEP,OAAI,MACF,MAAK,KAAK;IACR,KAAK;IACL,OAAO;KAAE,KAAK;KAAQ,MAAM;KAAiB,MAAM;KAAgB;IACnE,UAAU;IACX,CAAC;AAGJ,QAAK,KACH;IACE,KAAK;IACL,OAAO;KAAE,KAAK;KAAQ,MAAM;KAAa,OAAO;KAAS,MAAM;KAAsB;IACrF,UAAU;IACX,EACD;IACE,KAAK;IACL,OAAO;KAAE,KAAK;KAAQ,MAAM;KAAa,OAAO;KAAS,MAAM;KAAsB;IACrF,UAAU;IACX,EACD;IACE,KAAK;IACL,OAAO;KAAE,KAAK;KAAoB,OAAO;KAAW,MAAM;KAAyB;IACnF,UAAU;IACX,CACF;AAED,OAAI,iBACF,MAAK,KAAK;IACR,KAAK;IACL,OAAO;KAAE,KAAK;KAAY,MAAM;KAAqB;IACrD,UAAU;IACX,CAAC;AAGJ,QAAK,KAAK;IACR,KAAK;IACL,OAAO;KAAE,MAAM;KAAe,SAAS;KAAY;IACnD,UAAU;IACX,CAAC;AAEF,UAAO;;EAGT,MAAM,iBAAiB;AACrB,OAAI,CAAC,QAAS;AAGd,SAAM,mBAAmB,KAAK,MAAM,MAAM,OAAO,QAAQ,OAAO,YAAY,IAAI,QAAQ,YAAY,iBAAiB,iBAAiB;AAGtI,OAAI,OAAO,QACT,MAAK,MAAM,CAAC,QAAQ,iBAAiB,OAAO,QAAQ,OAAO,QAAQ,CACjE,OAAM,mBAAmB,KAAK,MAAM,MAAM,aAAa,QAAQ,aAAa,YAAY,GAAG,OAAO,IAAI,QAAQ,YAAY,iBAAiB,iBAAiB;;EAInK;;;;;AAMH,SAAS,oBAAoB,UAAkB,SAAyB;AAKtE,QAAO,oDAHc,SAAS,MAAM,oBAAoB,GACzB,MAAM,YAE8B;;;;;;qBAMhD,gBAAgB,SAAS,CAAC;oBAC3B,gBAAgB,QAAQ,CAAC;;;AAI7C,SAAS,gBAAgB,KAAqB;AAC5C,QAAO,IACJ,QAAQ,cAAc,GAAG,CACzB,QAAQ,eAAe,GAAG,CAC1B,MAAM;;;;;;AAOX,SAAS,oBACP,KACA,QACA,SAC4E;AAC5E,KAAI,CAAC,OAAO,QAAS,QAAO;AAE5B,MAAK,MAAM,CAAC,QAAQ,iBAAiB,OAAO,QAAQ,OAAO,QAAQ,EAAE;EACnE,MAAM,SAAS,IAAI,OAAO;AAC1B,MAAI,IAAI,WAAW,OAAO,CACxB,QAAO;GACL;GACA;GACA,QAAQ,aAAa;GACrB,YAAY,KAAK,SAAS,aAAa,OAAO;GAC/C;;AAGL,QAAO;;;;;;AAOT,eAAe,mBAEb,SACA,QACA,YACA,QACA,QACA,YACA,iBACA,kBACe;CACf,MAAM,aAAa,KAAK,SAAS,OAAO;AACxC,KAAI,CAAC,WAAW,WAAW,EAAE;AAE3B,UAAQ,KAAK,oCAAoC,aAAa;AAC9D;;AAMF,KAHc,OAAO,SAAS,OAAO,EAG1B;EACT,MAAM,aAAa,MAAM,SAAS,YAAY,QAAQ;EACtD,IAAI,WAAW;AAEf,MAAI,YAAY;GACd,MAAM,WAAW,KAAK,SAAS,WAAW;AAC1C,OAAI,WAAW,SAAS,CAEtB,YAAW,oBAAoB,YADf,MAAM,SAAS,UAAU,QAAQ,CACE;;AAIvD,OAAK,SAAS;GACZ,MAAM;GACN,UAAU,GAAG,OAAO;GACpB,QAAQ;GACT,CAAC;;AAIJ,MAAK,MAAM,EAAE,MAAM,UAAU,OAAO;EAClC,MAAM,YAAY,MAAM,YAAY,YAAY,KAAK;AACrD,MAAI,UACF,MAAK,SAAS;GACZ,MAAM;GACN,UAAU,GAAG,SAAS;GACtB,QAAQ;GACT,CAAC;;CAKN,MAAM,MAAM,MAAM,YAAY,WAAW;AACzC,KAAI,IACF,MAAK,SAAS;EACZ,MAAM;EACN,UAAU,GAAG,OAAO;EACpB,QAAQ;EACT,CAAC;AAIJ,KAAI,kBAAkB;EACpB,MAAM,iBAAiB,SAAS,IAAI,OAAO,MAAM,GAAG,GAAG,KAAK;EAC5D,MAAM,WAAW;GACf,MAAM,OAAO,QAAQ;GACrB,YAAY,OAAO,QAAQ;GAC3B,OAAO,CACL;IAAE,KAAK,GAAG,eAAe;IAAgB,OAAO;IAAW,MAAM;IAAa,EAC9E;IAAE,KAAK,GAAG,eAAe;IAAgB,OAAO;IAAW,MAAM;IAAa,CAC/E;GACD,aAAa;GACb,kBAAkB;GAClB,SAAS;GACV;AAED,OAAK,SAAS;GACZ,MAAM;GACN,UAAU,GAAG,OAAO;GACpB,QAAQ,KAAK,UAAU,UAAU,MAAM,EAAE;GAC1C,CAAC;;;;;;;;;;;;;AAcN,SAAgB,aACd,QACA,QACqE;CACrE,MAAM,oBAAoB,UAAU,OAAO,UAAU;CACrD,MAAM,SAAS,oBAAoB,IAAI,WAAW;CAClD,MAAM,SAAS,oBAAoB,OAAO,QAAS,QAAS,SAAS,OAAO,QAAQ,SAAS,OAAO;CAEpG,MAAM,QAA6E,EAAE;AAErF,KAAI,MACF,OAAM,KAAK;EAAE,KAAK;EAAQ,MAAM;EAAiB,MAAM,GAAG,OAAO;EAAe,CAAC;AAGnF,OAAM,KACJ;EAAE,KAAK;EAAQ,MAAM;EAAa,OAAO;EAAS,MAAM,GAAG,OAAO;EAAqB,EACvF;EAAE,KAAK;EAAQ,MAAM;EAAa,OAAO;EAAS,MAAM,GAAG,OAAO;EAAqB,EACvF;EAAE,KAAK;EAAoB,OAAO;EAAW,MAAM,GAAG,OAAO;EAAwB,CACtF;AAED,KAAI,OAAO,aAAa,MACtB,OAAM,KAAK;EAAE,KAAK;EAAY,MAAM,GAAG,OAAO;EAAoB,CAAC;AAGrE,QAAO;;AAGT,eAAe,YAAY,OAAe,MAA0C;AAClF,KAAI;AAEF,SAAO,OADO,MAAM,OAAO,SAAS,MAAM,MAAM,EAAE,WAAW,EAAE,EAC5C,MAAM,CAAC,OAAO,MAAM,MAAM;GAAE,KAAK;GAAW,YAAY;IAAE,GAAG;IAAG,GAAG;IAAG,GAAG;IAAG,OAAO;IAAG;GAAE,CAAQ,CAAC,KAAK,CAAC,UAAU;SAC9H;AACN,oBAAkB;AAClB,SAAO;;;AAIX,eAAe,YAAY,OAA2C;AACpE,KAAI;EACF,MAAM,QAAQ,MAAM,OAAO,SAAS,MAAM,MAAM,EAAE,WAAW,EAAE;EAC/D,MAAM,QAAQ,MAAM,MAAM,MAAM,CAAC,OAAO,IAAI,IAAI;GAAE,KAAK;GAAW,YAAY;IAAE,GAAG;IAAG,GAAG;IAAG,GAAG;IAAG,OAAO;IAAG;GAAE,CAAQ,CAAC,KAAK,CAAC,UAAU;EACvI,MAAM,QAAQ,MAAM,MAAM,MAAM,CAAC,OAAO,IAAI,IAAI;GAAE,KAAK;GAAW,YAAY;IAAE,GAAG;IAAG,GAAG;IAAG,GAAG;IAAG,OAAO;IAAG;GAAE,CAAQ,CAAC,KAAK,CAAC,UAAU;AAGvI,SAAO,kBAAkB,CACvB;GAAE,QAAQ;GAAO,MAAM;GAAI,EAC3B;GAAE,QAAQ;GAAO,MAAM;GAAI,CAC5B,CAAC;SACI;AACN,oBAAkB;AAClB,SAAO;;;;AAUX,SAAgB,kBAAkB,SAAiC;CACjE,MAAM,aAAa;CACnB,MAAM,eAAe;CACrB,MAAM,UAAU,eAAe,QAAQ;CACvC,IAAI,aAAa,aAAa;CAG9B,MAAM,SAAS,OAAO,MAAM,WAAW;AACvC,QAAO,cAAc,GAAG,EAAE;AAC1B,QAAO,cAAc,GAAG,EAAE;AAC1B,QAAO,cAAc,QAAQ,QAAQ,EAAE;CAGvC,MAAM,aAAa,OAAO,MAAM,QAAQ;CACxC,MAAM,cAAwB,EAAE;AAEhC,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,QAAQ,QAAQ;EACtB,MAAM,SAAS,IAAI;AACnB,aAAW,WAAW,MAAM,SAAS,MAAM,IAAI,MAAM,MAAM,OAAO;AAClE,aAAW,WAAW,MAAM,SAAS,MAAM,IAAI,MAAM,MAAM,SAAS,EAAE;AACtE,aAAW,WAAW,GAAG,SAAS,EAAE;AACpC,aAAW,WAAW,GAAG,SAAS,EAAE;AACpC,aAAW,cAAc,GAAG,SAAS,EAAE;AACvC,aAAW,cAAc,IAAI,SAAS,EAAE;AACxC,aAAW,cAAc,MAAM,OAAO,QAAQ,SAAS,EAAE;AACzD,aAAW,cAAc,YAAY,SAAS,GAAG;AAEjD,gBAAc,MAAM,OAAO;AAC3B,cAAY,KAAK,MAAM,OAAO;;AAGhC,QAAO,OAAO,OAAO;EAAC;EAAQ;EAAY,GAAG;EAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"favicon.js","names":[],"sources":["../src/favicon.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[zero:favicon] sharp not installed — favicons will not be generated. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Favicon generation plugin ──────────────────────────────────────────────\n//\n// Generates all favicon formats from a single source file (SVG or PNG):\n// - favicon.ico (16x16 + 32x32 combined)\n// - favicon.svg (copied if source is SVG)\n// - apple-touch-icon.png (180x180)\n// - icon-192.png (for web manifest)\n// - icon-512.png (for web manifest)\n// - site.webmanifest\n//\n// Usage:\n// import { faviconPlugin } from \"@pyreon/zero\"\n// export default { plugins: [zero(), faviconPlugin({ source: \"./icon.svg\" })] }\n\nexport interface FaviconLocaleConfig {\n /** Locale-specific source icon (SVG or PNG). */\n source: string\n /** Optional dark mode variant for this locale. */\n darkSource?: string\n}\n\nexport interface FaviconPluginConfig {\n /** Path to the source icon (SVG or PNG, at least 512x512 for PNG). */\n source: string\n /** Theme color for web manifest. Default: \"#ffffff\" */\n themeColor?: string\n /** Background color for web manifest. Default: \"#ffffff\" */\n backgroundColor?: string\n /** App name for web manifest. Uses package.json name if not set. */\n name?: string\n /** Generate web manifest. Default: true */\n manifest?: boolean\n /**\n * Dark mode favicon (SVG only).\n * When provided, the SVG favicon uses prefers-color-scheme media query\n * to switch between light and dark variants.\n */\n darkSource?: string\n /**\n * Locale-specific icon overrides. Each key is a locale code,\n * value is a source icon (and optional dark variant).\n * Locales not in this map use the base `source`.\n *\n * Generated files are placed under `/{locale}/` prefix:\n * /de/favicon.svg, /de/favicon-32x32.png, etc.\n *\n * @example\n * ```ts\n * faviconPlugin({\n * source: \"./icon.svg\",\n * locales: {\n * de: { source: \"./icon-de.svg\" },\n * cs: { source: \"./icon-cs.svg\" },\n * },\n * })\n * ```\n */\n locales?: Record<string, FaviconLocaleConfig>\n /**\n * Dev mode favicon — shown only during development to distinguish\n * dev tabs from production. Can be:\n * - A path to a separate icon file\n * - `true` to auto-generate a dev badge (grayscale + \"DEV\" overlay)\n *\n * @example\n * ```ts\n * faviconPlugin({\n * source: \"./icon.svg\",\n * devSource: \"./icon-dev.svg\", // custom dev icon\n * // OR\n * devSource: true, // auto-generate grayscale badge\n * })\n * ```\n */\n devSource?: string | boolean\n}\n\ninterface FaviconSize {\n size: number\n name: string\n}\n\nconst SIZES: FaviconSize[] = [\n { size: 16, name: 'favicon-16x16.png' },\n { size: 32, name: 'favicon-32x32.png' },\n { size: 180, name: 'apple-touch-icon.png' },\n { size: 192, name: 'icon-192.png' },\n { size: 512, name: 'icon-512.png' },\n]\n\n/**\n * Favicon generation Vite plugin.\n *\n * Generates all required favicon formats at build time from a single source.\n * In dev mode, serves the source directly.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { faviconPlugin } from \"@pyreon/zero\"\n *\n * export default {\n * plugins: [faviconPlugin({ source: \"./src/assets/icon.svg\" })],\n * }\n * ```\n */\nexport function faviconPlugin(config: FaviconPluginConfig): Plugin {\n const themeColor = config.themeColor ?? '#ffffff'\n const backgroundColor = config.backgroundColor ?? '#ffffff'\n const generateManifest = config.manifest !== false\n\n let root = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-favicon',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n isBuild = resolvedConfig.command === 'build'\n },\n\n // Dev server: serve generated favicons on-the-fly\n configureServer(server) {\n const sourcePath = join(root, config.source)\n const darkPath = config.darkSource ? join(root, config.darkSource) : null\n const devSourcePath = typeof config.devSource === 'string'\n ? join(root, config.devSource)\n : null\n const autoDevBadge = config.devSource === true\n const devCache = new Map<string, Uint8Array>()\n\n /** Resolve source path for a request — handles dark variants and dev badge. */\n function resolveSourceForDev(baseName: string, defaultSource: string): string {\n // Dark variant: favicon-dark-32x32.png → use darkSource\n if (darkPath && baseName.includes('-dark-')) return darkPath\n // Light variant: favicon-light-32x32.png → use source\n if (baseName.includes('-light-')) return defaultSource\n return defaultSource\n }\n\n server.middlewares.use(async (req, res, next) => {\n const url = req.url ?? ''\n\n // Resolve locale-specific source\n const localeSource = resolveLocaleSource(url, config, root)\n const svgUrl = localeSource ? localeSource.url : url\n const svgPath = localeSource ? localeSource.sourcePath : sourcePath\n const isSvgSource = localeSource ? localeSource.source.endsWith('.svg') : config.source.endsWith('.svg')\n\n // Serve favicon.svg — in dev, add dev badge overlay if configured\n if (svgUrl.endsWith('/favicon.svg') && isSvgSource) {\n try {\n let content = await readFile(svgPath, 'utf-8')\n if (autoDevBadge) content = addDevBadgeToSvg(content)\n else if (devSourcePath && existsSync(devSourcePath)) {\n content = await readFile(devSourcePath, 'utf-8')\n }\n res.setHeader('Content-Type', 'image/svg+xml')\n res.end(content)\n return\n } catch { /* fall through */ }\n }\n\n // Serve generated PNGs on-demand — supports dark variants + dev badge\n const baseName = svgUrl.split('/').pop() ?? ''\n // Strip light-/dark- prefix for size matching\n const cleanName = baseName.replace(/-?(light|dark)-/, '-')\n const sizeMatch = SIZES.find((s) => s.name === cleanName || baseName === s.name)\n if (sizeMatch) {\n const resolvedSource = resolveSourceForDev(baseName, svgPath)\n const cacheKey = `${resolvedSource}:${sizeMatch.size}:${autoDevBadge}`\n let png = devCache.get(cacheKey)\n if (!png) {\n let result = await resizeToPng(resolvedSource, sizeMatch.size)\n if (result && autoDevBadge) {\n result = await addDevBadgeToPng(result, sizeMatch.size)\n }\n if (result) {\n png = result\n devCache.set(cacheKey, result)\n }\n }\n if (png) {\n res.setHeader('Content-Type', 'image/png')\n res.setHeader('Cache-Control', 'no-cache')\n res.end(Buffer.from(png))\n return\n }\n }\n\n // Serve generated ICO on-demand\n if (baseName === 'favicon.ico') {\n const cacheKey = `ico:${svgPath}`\n let ico: Uint8Array | undefined = devCache.get(cacheKey)\n if (!ico) {\n const result = await generateIco(svgPath)\n if (result) {\n ico = result\n devCache.set(cacheKey, result)\n }\n }\n if (ico) {\n res.setHeader('Content-Type', 'image/x-icon')\n res.setHeader('Cache-Control', 'no-cache')\n res.end(Buffer.from(ico))\n return\n }\n }\n\n // Serve manifest (supports /{locale}/site.webmanifest)\n if (baseName === 'site.webmanifest' && generateManifest) {\n const prefix = localeSource ? `/${localeSource.locale}` : ''\n const manifest = {\n name: config.name ?? 'App',\n short_name: config.name ?? 'App',\n icons: [\n { src: `${prefix}/icon-192.png`, sizes: '192x192', type: 'image/png' },\n { src: `${prefix}/icon-512.png`, sizes: '512x512', type: 'image/png' },\n ],\n theme_color: themeColor,\n background_color: backgroundColor,\n display: 'standalone',\n }\n res.setHeader('Content-Type', 'application/manifest+json')\n res.end(JSON.stringify(manifest, null, 2))\n return\n }\n\n next()\n })\n },\n\n // Inject favicon <link> tags into HTML\n transformIndexHtml() {\n const isSvg = config.source.endsWith('.svg')\n const hasDark = !!config.darkSource\n const tags: Array<{\n tag: string\n attrs: Record<string, string>\n injectTo: 'head'\n }> = []\n\n // SVG favicon (with prefers-color-scheme media query when dark variant exists)\n if (isSvg) {\n tags.push({\n tag: 'link',\n attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },\n injectTo: 'head',\n })\n }\n\n if (hasDark) {\n // Dual-variant PNG/ICO favicons — light active, dark hidden via media=\"not all\".\n // The themeScript and initTheme() swap these based on the resolved theme.\n const lightAttrs = { 'data-favicon-theme': 'light' }\n const darkAttrs = { 'data-favicon-theme': 'dark', media: 'not all' }\n\n tags.push(\n { tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-light-32x32.png', ...lightAttrs }, injectTo: 'head' },\n { tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-dark-32x32.png', ...darkAttrs }, injectTo: 'head' },\n { tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-light-16x16.png', ...lightAttrs }, injectTo: 'head' },\n { tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-dark-16x16.png', ...darkAttrs }, injectTo: 'head' },\n { tag: 'link', attrs: { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon-light.png', ...lightAttrs }, injectTo: 'head' },\n { tag: 'link', attrs: { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon-dark.png', ...darkAttrs }, injectTo: 'head' },\n )\n } else {\n // Single-variant (no dark mode)\n tags.push(\n { tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' }, injectTo: 'head' },\n { tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' }, injectTo: 'head' },\n { tag: 'link', attrs: { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }, injectTo: 'head' },\n )\n }\n\n if (generateManifest) {\n tags.push({\n tag: 'link',\n attrs: { rel: 'manifest', href: '/site.webmanifest' },\n injectTo: 'head',\n })\n }\n\n tags.push({\n tag: 'meta',\n attrs: { name: 'theme-color', content: themeColor },\n injectTo: 'head',\n })\n\n return tags\n },\n\n async generateBundle() {\n if (!isBuild) return\n\n // Generate favicons for the base (default) source\n await generateFaviconSet.call(this, root, config.source, config.darkSource, '', config, themeColor, backgroundColor, generateManifest)\n\n // Generate locale-specific favicon sets\n if (config.locales) {\n for (const [locale, localeConfig] of Object.entries(config.locales)) {\n await generateFaviconSet.call(this, root, localeConfig.source, localeConfig.darkSource, `${locale}/`, config, themeColor, backgroundColor, generateManifest)\n }\n }\n },\n }\n}\n\n/**\n * Wrap two SVGs into a single SVG that switches based on prefers-color-scheme.\n */\nfunction wrapSvgWithDarkMode(lightSvg: string, darkSvg: string): string {\n // Extract viewBox from light SVG\n const viewBoxMatch = lightSvg.match(/viewBox=\"([^\"]*)\"/)\n const viewBox = viewBoxMatch?.[1] ?? '0 0 32 32'\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"${viewBox}\">\n <style>\n :root { color-scheme: light dark; }\n @media (prefers-color-scheme: dark) { .light { display: none; } }\n @media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) { .dark { display: none; } }\n </style>\n <g class=\"light\">${stripSvgWrapper(lightSvg)}</g>\n <g class=\"dark\">${stripSvgWrapper(darkSvg)}</g>\n</svg>`\n}\n\nfunction stripSvgWrapper(svg: string): string {\n return svg\n .replace(/<svg[^>]*>/, '')\n .replace(/<\\/svg>\\s*$/, '')\n .trim()\n}\n\n/**\n * Resolve the source path for a locale-prefixed favicon URL.\n * Returns null if the URL is not locale-prefixed or locale has no override.\n */\nfunction resolveLocaleSource(\n url: string,\n config: FaviconPluginConfig,\n rootDir: string,\n): { locale: string; url: string; source: string; sourcePath: string } | null {\n if (!config.locales) return null\n\n for (const [locale, localeConfig] of Object.entries(config.locales)) {\n const prefix = `/${locale}/`\n if (url.startsWith(prefix)) {\n return {\n locale,\n url,\n source: localeConfig.source,\n sourcePath: join(rootDir, localeConfig.source),\n }\n }\n }\n return null\n}\n\n/**\n * Generate a complete favicon set (SVG, PNGs, ICO, manifest) with a file prefix.\n * Called once for base (prefix = '') and once per locale (prefix = '{locale}/').\n */\nasync function generateFaviconSet(\n this: any,\n rootDir: string,\n source: string,\n darkSource: string | undefined,\n prefix: string,\n config: FaviconPluginConfig,\n themeColor: string,\n backgroundColor: string,\n generateManifest: boolean,\n): Promise<void> {\n const sourcePath = join(rootDir, source)\n if (!existsSync(sourcePath)) {\n // oxlint-disable-next-line no-console\n console.warn(`[zero:favicon] Source not found: ${sourcePath}`)\n return\n }\n\n const isSvg = source.endsWith('.svg')\n\n // Copy SVG as favicon.svg\n if (isSvg) {\n const svgContent = await readFile(sourcePath, 'utf-8')\n let finalSvg = svgContent\n\n if (darkSource) {\n const darkPath = join(rootDir, darkSource)\n if (existsSync(darkPath)) {\n const darkSvg = await readFile(darkPath, 'utf-8')\n finalSvg = wrapSvgWithDarkMode(svgContent, darkSvg)\n }\n }\n\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}favicon.svg`,\n source: finalSvg,\n })\n }\n\n // Generate PNG sizes via sharp\n if (darkSource) {\n // Dual-variant: generate light + dark PNGs with prefixed names\n const darkPath = join(rootDir, darkSource)\n const darkExists = existsSync(darkPath)\n\n for (const { size, name } of SIZES) {\n // Light variant\n const lightName = name.replace(/^(favicon-)/, '$1light-').replace(/^(apple-touch-icon)/, '$1-light').replace(/^(icon-)/, '$1light-')\n const lightPng = await resizeToPng(sourcePath, size)\n if (lightPng) {\n this.emitFile({ type: 'asset', fileName: `${prefix}${lightName}`, source: lightPng })\n }\n\n // Dark variant\n if (darkExists) {\n const darkName = name.replace(/^(favicon-)/, '$1dark-').replace(/^(apple-touch-icon)/, '$1-dark').replace(/^(icon-)/, '$1dark-')\n const darkPng = await resizeToPng(darkPath, size)\n if (darkPng) {\n this.emitFile({ type: 'asset', fileName: `${prefix}${darkName}`, source: darkPng })\n }\n }\n }\n\n // Also generate standard names (used by manifest + external references)\n for (const { size, name } of SIZES) {\n const pngBuffer = await resizeToPng(sourcePath, size)\n if (pngBuffer) {\n this.emitFile({ type: 'asset', fileName: `${prefix}${name}`, source: pngBuffer })\n }\n }\n } else {\n // Single-variant\n for (const { size, name } of SIZES) {\n const pngBuffer = await resizeToPng(sourcePath, size)\n if (pngBuffer) {\n this.emitFile({ type: 'asset', fileName: `${prefix}${name}`, source: pngBuffer })\n }\n }\n }\n\n // Generate favicon.ico (16 + 32)\n const ico = await generateIco(sourcePath)\n if (ico) {\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}favicon.ico`,\n source: ico,\n })\n }\n\n // Generate web manifest\n if (generateManifest) {\n const manifestPrefix = prefix ? `/${prefix.slice(0, -1)}` : ''\n const manifest = {\n name: config.name ?? 'App',\n short_name: config.name ?? 'App',\n icons: [\n { src: `${manifestPrefix}/icon-192.png`, sizes: '192x192', type: 'image/png' },\n { src: `${manifestPrefix}/icon-512.png`, sizes: '512x512', type: 'image/png' },\n ],\n theme_color: themeColor,\n background_color: backgroundColor,\n display: 'standalone',\n }\n\n this.emitFile({\n type: 'asset',\n fileName: `${prefix}site.webmanifest`,\n source: JSON.stringify(manifest, null, 2),\n })\n }\n}\n\n/**\n * Get favicon link tags for a specific locale.\n * Returns link objects suitable for `useHead()` or direct HTML injection.\n *\n * @example\n * ```ts\n * const links = faviconLinks(\"de\", { source: \"./icon.svg\", locales: { de: { source: \"./icon-de.svg\" } } })\n * // → [{ rel: \"icon\", type: \"image/svg+xml\", href: \"/de/favicon.svg\" }, ...]\n * ```\n */\nexport function faviconLinks(\n locale: string | undefined,\n config: FaviconPluginConfig,\n): Array<{ rel: string; type?: string; sizes?: string; href: string }> {\n const hasLocaleOverride = locale && config.locales?.[locale]\n const prefix = hasLocaleOverride ? `/${locale}` : ''\n const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')\n\n const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []\n\n if (isSvg) {\n links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })\n }\n\n links.push(\n { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${prefix}/favicon-32x32.png` },\n { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${prefix}/favicon-16x16.png` },\n { rel: 'apple-touch-icon', sizes: '180x180', href: `${prefix}/apple-touch-icon.png` },\n )\n\n if (config.manifest !== false) {\n links.push({ rel: 'manifest', href: `${prefix}/site.webmanifest` })\n }\n\n return links\n}\n\nasync function resizeToPng(input: string, size: number): Promise<Uint8Array | null> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n return await sharp(input).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } } as any).png().toBuffer()\n } catch {\n warnSharpMissing()\n return null\n }\n}\n\nasync function generateIco(input: string): Promise<Uint8Array | null> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const png16 = await sharp(input).resize(16, 16, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } } as any).png().toBuffer()\n const png32 = await sharp(input).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } } as any).png().toBuffer()\n\n // ICO format: header + directory entries + PNG data\n return createIcoFromPngs([\n { buffer: png16, size: 16 },\n { buffer: png32, size: 32 },\n ])\n } catch {\n warnSharpMissing()\n return null\n }\n}\n\nexport interface IcoEntry {\n buffer: Buffer\n size: number\n}\n\n/** @internal Exported for testing */\nexport function createIcoFromPngs(entries: IcoEntry[]): Uint8Array {\n const headerSize = 6\n const dirEntrySize = 16\n const dirSize = dirEntrySize * entries.length\n let dataOffset = headerSize + dirSize\n\n // ICO header\n const header = Buffer.alloc(headerSize)\n header.writeUInt16LE(0, 0) // reserved\n header.writeUInt16LE(1, 2) // type: icon\n header.writeUInt16LE(entries.length, 4) // count\n\n // Directory entries\n const dirEntries = Buffer.alloc(dirSize)\n const dataBuffers: Buffer[] = []\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i]!\n const offset = i * dirEntrySize\n dirEntries.writeUInt8(entry.size === 256 ? 0 : entry.size, offset) // width\n dirEntries.writeUInt8(entry.size === 256 ? 0 : entry.size, offset + 1) // height\n dirEntries.writeUInt8(0, offset + 2) // palette\n dirEntries.writeUInt8(0, offset + 3) // reserved\n dirEntries.writeUInt16LE(1, offset + 4) // color planes\n dirEntries.writeUInt16LE(32, offset + 6) // bits per pixel\n dirEntries.writeUInt32LE(entry.buffer.length, offset + 8) // size\n dirEntries.writeUInt32LE(dataOffset, offset + 12) // offset\n\n dataOffset += entry.buffer.length\n dataBuffers.push(entry.buffer)\n }\n\n return Buffer.concat([header, dirEntries, ...dataBuffers])\n}\n\n// ─── Dev badge helpers ──────────────────────────────────────────────────────\n\n/**\n * Add a \"DEV\" badge overlay to an SVG string.\n * Adds a small colored circle with \"DEV\" text in the bottom-right corner.\n */\nfunction addDevBadgeToSvg(svg: string): string {\n const viewBoxMatch = svg.match(/viewBox=\"([^\"]*)\"/)\n const viewBox = viewBoxMatch?.[1] ?? '0 0 32 32'\n const [, , w, h] = viewBox.split(' ').map(Number)\n const size = Math.min(w ?? 32, h ?? 32)\n const r = size * 0.28\n const cx = (w ?? 32) - r\n const cy = (h ?? 32) - r\n const fontSize = r * 0.85\n\n const badge = `<circle cx=\"${cx}\" cy=\"${cy}\" r=\"${r}\" fill=\"#ef4444\" stroke=\"white\" stroke-width=\"${size * 0.03}\"/>` +\n `<text x=\"${cx}\" y=\"${cy}\" font-size=\"${fontSize}\" font-weight=\"bold\" fill=\"white\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"sans-serif\">D</text>`\n\n // Insert badge before closing </svg>\n return svg.replace(/<\\/svg>\\s*$/, `${badge}</svg>`)\n}\n\n/**\n * Add a \"DEV\" badge to a PNG buffer via sharp composite.\n * Composites a red circle with \"D\" in the bottom-right corner.\n */\nasync function addDevBadgeToPng(pngBuffer: Uint8Array, size: number): Promise<Uint8Array> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const r = Math.round(size * 0.28)\n const d = r * 2\n const fontSize = Math.round(r * 0.85)\n\n const badgeSvg = `<svg width=\"${d}\" height=\"${d}\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle cx=\"${r}\" cy=\"${r}\" r=\"${r}\" fill=\"#ef4444\"/>\n <text x=\"${r}\" y=\"${r}\" font-size=\"${fontSize}\" font-weight=\"bold\" fill=\"white\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"sans-serif\">D</text>\n </svg>`\n\n const badgePng = await sharp(Buffer.from(badgeSvg)).png().toBuffer()\n\n return await (sharp(Buffer.from(pngBuffer)) as any)\n .composite([{\n input: badgePng,\n gravity: 'southeast',\n }])\n .png()\n .toBuffer()\n } catch {\n // sharp not available — return original\n return pngBuffer\n }\n}\n"],"mappings":"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"}
|