@pyreon/zero 0.12.7 → 0.12.9

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/favicon.js CHANGED
@@ -271,6 +271,12 @@ function faviconPlugin(config) {
271
271
  },
272
272
  injectTo: "head"
273
273
  });
274
+ if (hasDark) tags.push({
275
+ tag: "script",
276
+ attrs: {},
277
+ injectTo: "head",
278
+ children: `(function(){try{var t=localStorage.getItem("zero-theme");var r=t==="light"?"light":t==="dark"?"dark":window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light";document.querySelectorAll("[data-favicon-theme]").forEach(function(l){l.media=l.dataset.faviconTheme===r?"":"not all"})}catch(e){}})()`
279
+ });
274
280
  return tags;
275
281
  },
276
282
  async generateBundle() {
@@ -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 * 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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"}
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 // Auto-inject favicon swap script when dark variant exists.\n // This runs in the blocking <head> before any render — no flash.\n // Reads theme from localStorage or OS preference, then swaps\n // data-favicon-theme media attributes.\n if (hasDark) {\n tags.push({\n tag: 'script',\n attrs: {},\n injectTo: 'head',\n children: `(function(){try{var t=localStorage.getItem(\"zero-theme\");var r=t===\"light\"?\"light\":t===\"dark\"?\"dark\":window.matchMedia(\"(prefers-color-scheme:dark)\").matches?\"dark\":\"light\";document.querySelectorAll(\"[data-favicon-theme]\").forEach(function(l){l.media=l.dataset.faviconTheme===r?\"\":\"not all\"})}catch(e){}})()`,\n } as any)\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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"}
@@ -0,0 +1,290 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) {
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ }
11
+ if (!no_symbols) {
12
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
13
+ }
14
+ return target;
15
+ };
16
+
17
+ //#endregion
18
+ //#region src/fs-router.ts
19
+ var fs_router_exports = /* @__PURE__ */ __exportAll({
20
+ filePathToUrlPath: () => filePathToUrlPath,
21
+ generateMiddlewareModule: () => generateMiddlewareModule,
22
+ generateRouteModule: () => generateRouteModule,
23
+ parseFileRoutes: () => parseFileRoutes,
24
+ scanRouteFiles: () => scanRouteFiles
25
+ });
26
+ const ROUTE_EXTENSIONS = [
27
+ ".tsx",
28
+ ".jsx",
29
+ ".ts",
30
+ ".js"
31
+ ];
32
+ /**
33
+ * Parse a set of file paths (relative to routes dir) into FileRoute objects.
34
+ *
35
+ * @param files Array of file paths like ["index.tsx", "users/[id].tsx"]
36
+ * @param defaultMode Default rendering mode from config
37
+ */
38
+ function parseFileRoutes(files, defaultMode = "ssr") {
39
+ return files.filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext))).map((filePath) => parseFilePath(filePath, defaultMode)).sort(sortRoutes);
40
+ }
41
+ function parseFilePath(filePath, defaultMode) {
42
+ let route = filePath;
43
+ for (const ext of ROUTE_EXTENSIONS) if (route.endsWith(ext)) {
44
+ route = route.slice(0, -ext.length);
45
+ break;
46
+ }
47
+ const fileName = getFileName(route);
48
+ const isLayout = fileName === "_layout";
49
+ const isError = fileName === "_error";
50
+ const isLoading = fileName === "_loading";
51
+ const isNotFound = fileName === "_404" || fileName === "_not-found";
52
+ const isCatchAll = route.includes("[...");
53
+ const parts = route.split("/");
54
+ parts.pop();
55
+ const dirPath = parts.filter((s) => !(s.startsWith("(") && s.endsWith(")"))).join("/");
56
+ const urlPath = filePathToUrlPath(route);
57
+ return {
58
+ filePath,
59
+ urlPath,
60
+ dirPath,
61
+ depth: urlPath === "/" ? 0 : urlPath.split("/").filter(Boolean).length,
62
+ isLayout,
63
+ isError,
64
+ isLoading,
65
+ isNotFound,
66
+ isCatchAll,
67
+ renderMode: defaultMode
68
+ };
69
+ }
70
+ /**
71
+ * Convert a file path (without extension) to a URL path pattern.
72
+ *
73
+ * Examples:
74
+ * "index" → "/"
75
+ * "about" → "/about"
76
+ * "users/index" → "/users"
77
+ * "users/[id]" → "/users/:id"
78
+ * "blog/[...slug]" → "/blog/:slug*"
79
+ * "(auth)/login" → "/login" (group stripped)
80
+ * "_layout" → "/" (layout marker)
81
+ */
82
+ function filePathToUrlPath(filePath) {
83
+ const segments = filePath.split("/");
84
+ const urlSegments = [];
85
+ for (const seg of segments) {
86
+ if (seg.startsWith("(") && seg.endsWith(")")) continue;
87
+ if (seg === "_layout" || seg === "_error" || seg === "_loading" || seg === "_404" || seg === "_not-found") continue;
88
+ if (seg === "index") continue;
89
+ const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/);
90
+ if (catchAll) {
91
+ urlSegments.push(`:${catchAll[1]}*`);
92
+ continue;
93
+ }
94
+ const dynamic = seg.match(/^\[(\w+)\]$/);
95
+ if (dynamic) {
96
+ urlSegments.push(`:${dynamic[1]}`);
97
+ continue;
98
+ }
99
+ urlSegments.push(seg);
100
+ }
101
+ return `/${urlSegments.join("/")}` || "/";
102
+ }
103
+ /** Sort routes: static before dynamic, catch-all last. */
104
+ function sortRoutes(a, b) {
105
+ if (a.isCatchAll !== b.isCatchAll) return a.isCatchAll ? 1 : -1;
106
+ if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1;
107
+ const aDynamic = a.urlPath.includes(":");
108
+ if (aDynamic !== b.urlPath.includes(":")) return aDynamic ? 1 : -1;
109
+ return a.urlPath.localeCompare(b.urlPath);
110
+ }
111
+ function getFileName(filePath) {
112
+ const parts = filePath.split("/");
113
+ return parts[parts.length - 1] ?? "";
114
+ }
115
+ /**
116
+ * Group flat file routes into a directory tree.
117
+ */
118
+ function getOrCreateChild(node, segment) {
119
+ let child = node.children.get(segment);
120
+ if (!child) {
121
+ child = {
122
+ pages: [],
123
+ children: /* @__PURE__ */ new Map()
124
+ };
125
+ node.children.set(segment, child);
126
+ }
127
+ return child;
128
+ }
129
+ function resolveNode(root, dirPath) {
130
+ let node = root;
131
+ if (dirPath) for (const segment of dirPath.split("/")) node = getOrCreateChild(node, segment);
132
+ return node;
133
+ }
134
+ function placeRoute(node, route) {
135
+ if (route.isLayout) node.layout = route;
136
+ else if (route.isError) node.error = route;
137
+ else if (route.isLoading) node.loading = route;
138
+ else if (route.isNotFound) node.notFound = route;
139
+ else node.pages.push(route);
140
+ }
141
+ function buildRouteTree(routes) {
142
+ const root = {
143
+ pages: [],
144
+ children: /* @__PURE__ */ new Map()
145
+ };
146
+ for (const route of routes) placeRoute(resolveNode(root, route.dirPath), route);
147
+ return root;
148
+ }
149
+ function generateRouteModule(files, routesDir, options) {
150
+ const tree = buildRouteTree(parseFileRoutes(files));
151
+ const imports = [];
152
+ let importCounter = 0;
153
+ const useStaticImports = options?.staticImports ?? false;
154
+ function nextImport(filePath, exportName = "default") {
155
+ const name = `_${importCounter++}`;
156
+ const fullPath = `${routesDir}/${filePath}`;
157
+ if (exportName === "default") imports.push(`import ${name} from "${fullPath}"`);
158
+ else imports.push(`import { ${exportName} as ${name} } from "${fullPath}"`);
159
+ return name;
160
+ }
161
+ function nextLazy(filePath, loadingName, errorName) {
162
+ const name = `_${importCounter++}`;
163
+ const fullPath = `${routesDir}/${filePath}`;
164
+ if (useStaticImports) imports.push(`import ${name} from "${fullPath}"`);
165
+ else {
166
+ const opts = [];
167
+ if (loadingName) opts.push(`loading: ${loadingName}`);
168
+ if (errorName) opts.push(`error: ${errorName}`);
169
+ const optsStr = opts.length > 0 ? `, { ${opts.join(", ")} }` : "";
170
+ imports.push(`const ${name} = lazy(() => import("${fullPath}")${optsStr})`);
171
+ }
172
+ return name;
173
+ }
174
+ function nextModuleImport(filePath) {
175
+ const name = `_m${importCounter++}`;
176
+ const fullPath = `${routesDir}/${filePath}`;
177
+ imports.push(`import * as ${name} from "${fullPath}"`);
178
+ return name;
179
+ }
180
+ function generatePageRoute(page, indent, loadingName, errorName, notFoundName) {
181
+ const mod = nextModuleImport(page.filePath);
182
+ const comp = nextLazy(page.filePath, loadingName, errorName);
183
+ const props = [
184
+ `${indent} path: ${JSON.stringify(page.urlPath)}`,
185
+ `${indent} component: ${comp}`,
186
+ `${indent} loader: ${mod}.loader`,
187
+ `${indent} beforeEnter: ${mod}.guard`,
188
+ `${indent} meta: { ...${mod}.meta, renderMode: ${mod}.renderMode }`
189
+ ];
190
+ if (errorName) props.push(`${indent} errorComponent: ${mod}.error || ${errorName}`);
191
+ if (notFoundName) props.push(`${indent} notFoundComponent: ${notFoundName}`);
192
+ return `${indent}{\n${props.join(",\n")}\n${indent}}`;
193
+ }
194
+ function wrapWithLayout(node, children, indent, errorName, notFoundName) {
195
+ const layout = node.layout;
196
+ const layoutMod = nextModuleImport(layout.filePath);
197
+ const layoutComp = nextImport(layout.filePath, "layout");
198
+ const props = [
199
+ `${indent}path: ${JSON.stringify(layout.urlPath)}`,
200
+ `${indent}component: ${layoutComp}`,
201
+ `${indent}loader: ${layoutMod}.loader`,
202
+ `${indent}beforeEnter: ${layoutMod}.guard`,
203
+ `${indent}meta: { ...${layoutMod}.meta, renderMode: ${layoutMod}.renderMode }`
204
+ ];
205
+ if (errorName) props.push(`${indent}errorComponent: ${errorName}`);
206
+ if (notFoundName) props.push(`${indent}notFoundComponent: ${notFoundName}`);
207
+ if (children.length > 0) props.push(`${indent}children: [\n${children.join(",\n")}\n${indent}]`);
208
+ return `${indent}{\n${props.map((p) => ` ${p}`).join(",\n")}\n${indent}}`;
209
+ }
210
+ /**
211
+ * Generate route definitions for a tree node.
212
+ */
213
+ function generateNode(node, depth) {
214
+ const indent = " ".repeat(depth + 1);
215
+ const errorName = node.error ? nextImport(node.error.filePath) : void 0;
216
+ const loadingName = node.loading ? nextImport(node.loading.filePath) : void 0;
217
+ const notFoundName = node.notFound ? nextImport(node.notFound.filePath) : void 0;
218
+ const childRouteDefs = [];
219
+ for (const [, childNode] of node.children) childRouteDefs.push(...generateNode(childNode, depth + 1));
220
+ const allChildren = [...node.pages.map((page) => generatePageRoute(page, indent, loadingName, errorName, notFoundName)), ...childRouteDefs];
221
+ if (node.layout) return [wrapWithLayout(node, allChildren, indent, errorName, notFoundName)];
222
+ return allChildren;
223
+ }
224
+ const routeDefs = generateNode(tree, 0);
225
+ return [
226
+ `import { lazy } from "@pyreon/router"`,
227
+ "",
228
+ ...imports,
229
+ "",
230
+ `function clean(routes) {`,
231
+ ` return routes.map(r => {`,
232
+ ` const c = {}`,
233
+ ` for (const k in r) if (r[k] !== undefined) c[k] = r[k]`,
234
+ ` if (c.children) c.children = clean(c.children)`,
235
+ ` return c`,
236
+ ` })`,
237
+ `}`,
238
+ "",
239
+ `export const routes = clean([`,
240
+ routeDefs.join(",\n"),
241
+ `])`
242
+ ].join("\n");
243
+ }
244
+ /**
245
+ * Generate a virtual module that maps URL patterns to their middleware exports.
246
+ * Used by the server entry to dispatch per-route middleware.
247
+ */
248
+ function generateMiddlewareModule(files, routesDir) {
249
+ const routes = parseFileRoutes(files);
250
+ const imports = [];
251
+ const entries = [];
252
+ let counter = 0;
253
+ for (const route of routes) {
254
+ if (route.isLayout || route.isError || route.isLoading || route.isNotFound) continue;
255
+ const name = `_mw${counter++}`;
256
+ const fullPath = `${routesDir}/${route.filePath}`;
257
+ imports.push(`import { middleware as ${name} } from "${fullPath}"`);
258
+ entries.push(` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`);
259
+ }
260
+ return [
261
+ ...imports,
262
+ "",
263
+ `export const routeMiddleware = [`,
264
+ entries.join(",\n"),
265
+ `].filter(e => e.middleware)`
266
+ ].join("\n");
267
+ }
268
+ /**
269
+ * Scan a directory for route files.
270
+ * Returns paths relative to the routes directory.
271
+ */
272
+ async function scanRouteFiles(routesDir) {
273
+ const { readdir } = await import("node:fs/promises");
274
+ const { join, relative } = await import("node:path");
275
+ const files = [];
276
+ async function walk(dir) {
277
+ const entries = await readdir(dir, { withFileTypes: true });
278
+ for (const entry of entries) {
279
+ const fullPath = join(dir, entry.name);
280
+ if (entry.isDirectory()) await walk(fullPath);
281
+ else if (ROUTE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) files.push(relative(routesDir, fullPath));
282
+ }
283
+ }
284
+ await walk(routesDir);
285
+ return files;
286
+ }
287
+
288
+ //#endregion
289
+ export { parseFileRoutes as a, generateRouteModule as i, fs_router_exports as n, scanRouteFiles as o, generateMiddlewareModule as r, filePathToUrlPath as t };
290
+ //# sourceMappingURL=fs-router-Dil4IKZR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-router-Dil4IKZR.js","names":[],"sources":["../src/fs-router.ts"],"sourcesContent":["import type { FileRoute, RenderMode } from './types'\n\n// ─── File-system route conventions ──────────────────────────────────────────\n//\n// src/routes/\n// _layout.tsx → layout for all routes\n// index.tsx → /\n// about.tsx → /about\n// users/\n// _layout.tsx → layout for /users/*\n// _loading.tsx → loading fallback for /users/*\n// _error.tsx → error boundary for /users/*\n// index.tsx → /users\n// [id].tsx → /users/:id\n// [id]/\n// settings.tsx → /users/:id/settings\n// blog/\n// [...slug].tsx → /blog/* (catch-all)\n//\n// Conventions:\n// [param] → dynamic segment → :param\n// [...param] → catch-all → :param*\n// _layout → layout wrapper — must use <RouterView /> to render child routes\n// (props.children is NOT passed — the router handles nesting)\n// _error → error component\n// _loading → loading component\n// _404 → not-found component (renders on 404)\n// _not-found → alias for _404\n// (group) → route group (directory ignored in URL)\n\nconst ROUTE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js']\n\n/**\n * Parse a set of file paths (relative to routes dir) into FileRoute objects.\n *\n * @param files Array of file paths like [\"index.tsx\", \"users/[id].tsx\"]\n * @param defaultMode Default rendering mode from config\n */\nexport function parseFileRoutes(files: string[], defaultMode: RenderMode = 'ssr'): FileRoute[] {\n return files\n .filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext)))\n .map((filePath) => parseFilePath(filePath, defaultMode))\n .sort(sortRoutes)\n}\n\nfunction parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {\n // Remove extension\n let route = filePath\n for (const ext of ROUTE_EXTENSIONS) {\n if (route.endsWith(ext)) {\n route = route.slice(0, -ext.length)\n break\n }\n }\n\n const fileName = getFileName(route)\n const isLayout = fileName === '_layout'\n const isError = fileName === '_error'\n const isLoading = fileName === '_loading'\n const isNotFound = fileName === '_404' || fileName === '_not-found'\n const isCatchAll = route.includes('[...')\n\n // Get directory path (strip groups for consistent grouping)\n const parts = route.split('/')\n parts.pop() // remove filename\n const dirPath = parts.filter((s) => !(s.startsWith('(') && s.endsWith(')'))).join('/')\n\n // Convert file path to URL pattern\n const urlPath = filePathToUrlPath(route)\n const depth = urlPath === '/' ? 0 : urlPath.split('/').filter(Boolean).length\n\n return {\n filePath,\n urlPath,\n dirPath,\n depth,\n isLayout,\n isError,\n isLoading,\n isNotFound,\n isCatchAll,\n renderMode: defaultMode,\n }\n}\n\n/**\n * Convert a file path (without extension) to a URL path pattern.\n *\n * Examples:\n * \"index\" → \"/\"\n * \"about\" → \"/about\"\n * \"users/index\" → \"/users\"\n * \"users/[id]\" → \"/users/:id\"\n * \"blog/[...slug]\" → \"/blog/:slug*\"\n * \"(auth)/login\" → \"/login\" (group stripped)\n * \"_layout\" → \"/\" (layout marker)\n */\nexport function filePathToUrlPath(filePath: string): string {\n const segments = filePath.split('/')\n const urlSegments: string[] = []\n\n for (const seg of segments) {\n // Skip route groups \"(name)\"\n if (seg.startsWith('(') && seg.endsWith(')')) continue\n\n // Skip special files\n if (seg === '_layout' || seg === '_error' || seg === '_loading' || seg === '_404' || seg === '_not-found') continue\n\n // \"index\" maps to the parent path\n if (seg === 'index') continue\n\n // Catch-all: [...param] → :param*\n const catchAll = seg.match(/^\\[\\.\\.\\.(\\w+)\\]$/)\n if (catchAll) {\n urlSegments.push(`:${catchAll[1]}*`)\n continue\n }\n\n // Dynamic: [param] → :param\n const dynamic = seg.match(/^\\[(\\w+)\\]$/)\n if (dynamic) {\n urlSegments.push(`:${dynamic[1]}`)\n continue\n }\n\n urlSegments.push(seg)\n }\n\n const path = `/${urlSegments.join('/')}`\n return path || '/'\n}\n\n/** Sort routes: static before dynamic, catch-all last. */\nfunction sortRoutes(a: FileRoute, b: FileRoute): number {\n // Catch-all routes go last\n if (a.isCatchAll !== b.isCatchAll) return a.isCatchAll ? 1 : -1\n // Layouts go first within same depth\n if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1\n // Static segments before dynamic\n const aDynamic = a.urlPath.includes(':')\n const bDynamic = b.urlPath.includes(':')\n if (aDynamic !== bDynamic) return aDynamic ? 1 : -1\n // Alphabetical\n return a.urlPath.localeCompare(b.urlPath)\n}\n\nfunction getFileName(filePath: string): string {\n const parts = filePath.split('/')\n return parts[parts.length - 1] ?? ''\n}\n\n// ─── Route generation (for Vite plugin) ─────────────────────────────────────\n\n/** Internal tree node for building nested route structures. */\ninterface RouteNode {\n /** Page routes at this directory level. */\n pages: FileRoute[]\n /** Layout file for this directory (if any). */\n layout?: FileRoute\n /** Error boundary file (if any). */\n error?: FileRoute\n /** Loading fallback file (if any). */\n loading?: FileRoute\n /** Not-found (404) file (if any). */\n notFound?: FileRoute\n /** Child directories. */\n children: Map<string, RouteNode>\n}\n\n/**\n * Group flat file routes into a directory tree.\n */\nfunction getOrCreateChild(node: RouteNode, segment: string): RouteNode {\n let child = node.children.get(segment)\n if (!child) {\n child = { pages: [], children: new Map() }\n node.children.set(segment, child)\n }\n return child\n}\n\nfunction resolveNode(root: RouteNode, dirPath: string): RouteNode {\n let node = root\n if (dirPath) {\n for (const segment of dirPath.split('/')) {\n node = getOrCreateChild(node, segment)\n }\n }\n return node\n}\n\nfunction placeRoute(node: RouteNode, route: FileRoute) {\n if (route.isLayout) node.layout = route\n else if (route.isError) node.error = route\n else if (route.isLoading) node.loading = route\n else if (route.isNotFound) node.notFound = route\n else node.pages.push(route)\n}\n\nfunction buildRouteTree(routes: FileRoute[]): RouteNode {\n const root: RouteNode = { pages: [], children: new Map() }\n for (const route of routes) {\n placeRoute(resolveNode(root, route.dirPath), route)\n }\n return root\n}\n\n/**\n * Generate a virtual module that exports a nested route tree.\n * Wires up layouts as parent routes with children, loaders, guards,\n * error/loading components, middleware, and meta from route module exports.\n */\nexport interface GenerateRouteModuleOptions {\n /**\n * When true, skip lazy() for route components and use static imports.\n * Use for SSG/prerender mode where all routes are rendered at build time\n * and code splitting provides no benefit. Avoids Rolldown warnings about\n * static + dynamic imports of the same module.\n */\n staticImports?: boolean\n}\n\nexport function generateRouteModule(\n files: string[],\n routesDir: string,\n options?: GenerateRouteModuleOptions,\n): string {\n const routes = parseFileRoutes(files)\n const tree = buildRouteTree(routes)\n const imports: string[] = []\n let importCounter = 0\n const useStaticImports = options?.staticImports ?? false\n\n function nextImport(filePath: string, exportName = 'default'): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n if (exportName === 'default') {\n imports.push(`import ${name} from \"${fullPath}\"`)\n } else {\n imports.push(`import { ${exportName} as ${name} } from \"${fullPath}\"`)\n }\n return name\n }\n\n function nextLazy(filePath: string, loadingName?: string, errorName?: string): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n\n if (useStaticImports) {\n // SSG mode: static import avoids Rolldown warnings about\n // static + dynamic imports of the same module\n imports.push(`import ${name} from \"${fullPath}\"`)\n } else {\n const opts: string[] = []\n if (loadingName) opts.push(`loading: ${loadingName}`)\n if (errorName) opts.push(`error: ${errorName}`)\n const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : ''\n imports.push(`const ${name} = lazy(() => import(\"${fullPath}\")${optsStr})`)\n }\n return name\n }\n\n function nextModuleImport(filePath: string): string {\n const name = `_m${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n imports.push(`import * as ${name} from \"${fullPath}\"`)\n return name\n }\n\n function generatePageRoute(\n page: FileRoute,\n indent: string,\n loadingName: string | undefined,\n errorName: string | undefined,\n notFoundName: string | undefined,\n ): string {\n const mod = nextModuleImport(page.filePath)\n const comp = nextLazy(page.filePath, loadingName, errorName)\n\n const props: string[] = [\n `${indent} path: ${JSON.stringify(page.urlPath)}`,\n `${indent} component: ${comp}`,\n `${indent} loader: ${mod}.loader`,\n `${indent} beforeEnter: ${mod}.guard`,\n `${indent} meta: { ...${mod}.meta, renderMode: ${mod}.renderMode }`,\n ]\n\n // Only emit errorComponent when there's an actual _error file in scope\n // or the route module exports an error component. Avoids referencing\n // undefined .error exports that produce noisy bundler warnings.\n if (errorName) {\n props.push(`${indent} errorComponent: ${mod}.error || ${errorName}`)\n }\n\n if (notFoundName) {\n props.push(`${indent} notFoundComponent: ${notFoundName}`)\n }\n\n return `${indent}{\\n${props.join(',\\n')}\\n${indent}}`\n }\n\n function wrapWithLayout(\n node: RouteNode,\n children: string[],\n indent: string,\n errorName: string | undefined,\n notFoundName: string | undefined,\n ): string {\n const layout = node.layout as FileRoute\n const layoutMod = nextModuleImport(layout.filePath)\n const layoutComp = nextImport(layout.filePath, 'layout')\n\n const props: string[] = [\n `${indent}path: ${JSON.stringify(layout.urlPath)}`,\n `${indent}component: ${layoutComp}`,\n `${indent}loader: ${layoutMod}.loader`,\n `${indent}beforeEnter: ${layoutMod}.guard`,\n `${indent}meta: { ...${layoutMod}.meta, renderMode: ${layoutMod}.renderMode }`,\n ]\n if (errorName) {\n props.push(`${indent}errorComponent: ${errorName}`)\n }\n if (notFoundName) {\n props.push(`${indent}notFoundComponent: ${notFoundName}`)\n }\n if (children.length > 0) {\n props.push(`${indent}children: [\\n${children.join(',\\n')}\\n${indent}]`)\n }\n\n return `${indent}{\\n${props.map((p) => ` ${p}`).join(',\\n')}\\n${indent}}`\n }\n\n /**\n * Generate route definitions for a tree node.\n */\n function generateNode(node: RouteNode, depth: number): string[] {\n const indent = ' '.repeat(depth + 1)\n\n const errorName = node.error ? nextImport(node.error.filePath) : undefined\n const loadingName = node.loading ? nextImport(node.loading.filePath) : undefined\n const notFoundName = node.notFound ? nextImport(node.notFound.filePath) : undefined\n\n const childRouteDefs: string[] = []\n for (const [, childNode] of node.children) {\n childRouteDefs.push(...generateNode(childNode, depth + 1))\n }\n\n const pageRouteDefs = node.pages.map((page) =>\n generatePageRoute(page, indent, loadingName, errorName, notFoundName),\n )\n\n const allChildren = [...pageRouteDefs, ...childRouteDefs]\n\n if (node.layout) {\n return [wrapWithLayout(node, allChildren, indent, errorName, notFoundName)]\n }\n return allChildren\n }\n\n const routeDefs = generateNode(tree, 0)\n\n return [\n `import { lazy } from \"@pyreon/router\"`,\n '',\n ...imports,\n '',\n // Filter out undefined properties at runtime\n `function clean(routes) {`,\n ` return routes.map(r => {`,\n ` const c = {}`,\n ` for (const k in r) if (r[k] !== undefined) c[k] = r[k]`,\n ` if (c.children) c.children = clean(c.children)`,\n ` return c`,\n ` })`,\n `}`,\n '',\n `export const routes = clean([`,\n routeDefs.join(',\\n'),\n `])`,\n ].join('\\n')\n}\n\n/**\n * Generate a virtual module that maps URL patterns to their middleware exports.\n * Used by the server entry to dispatch per-route middleware.\n */\nexport function generateMiddlewareModule(files: string[], routesDir: string): string {\n const routes = parseFileRoutes(files)\n const imports: string[] = []\n const entries: string[] = []\n let counter = 0\n\n for (const route of routes) {\n if (route.isLayout || route.isError || route.isLoading || route.isNotFound) continue\n const name = `_mw${counter++}`\n const fullPath = `${routesDir}/${route.filePath}`\n imports.push(`import { middleware as ${name} } from \"${fullPath}\"`)\n entries.push(` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`)\n }\n\n return [\n ...imports,\n '',\n `export const routeMiddleware = [`,\n entries.join(',\\n'),\n `].filter(e => e.middleware)`,\n ].join('\\n')\n}\n\n/**\n * Scan a directory for route files.\n * Returns paths relative to the routes directory.\n */\nexport async function scanRouteFiles(routesDir: string): Promise<string[]> {\n const { readdir } = await import('node:fs/promises')\n const { join, relative } = await import('node:path')\n\n const files: string[] = []\n\n async function walk(dir: string) {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory()) {\n await walk(fullPath)\n } else if (ROUTE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {\n files.push(relative(routesDir, fullPath))\n }\n }\n }\n\n await walk(routesDir)\n return files\n}\n"],"mappings":"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"}
@@ -9,6 +9,13 @@ function warnSharpMissing() {
9
9
  sharpWarned = true;
10
10
  console.warn("\n[zero:image] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\n");
11
11
  }
12
+ /** Built-in CDN providers. */
13
+ const cdnProviders = {
14
+ cloudinary: (cloudName) => (src, { width, quality, format }) => `https://res.cloudinary.com/${cloudName}/image/upload/w_${width},q_${quality},f_${format}/${src}`,
15
+ imgix: (domain) => (src, { width, quality, format }) => `https://${domain}.imgix.net/${src}?w=${width}&q=${quality}&fm=${format}&auto=format`,
16
+ vercel: () => (src, { width, quality }) => `/_vercel/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`,
17
+ bunny: (pullZone) => (src, { width, quality }) => `https://${pullZone}.b-cdn.net/${src}?width=${width}&quality=${quality}`
18
+ };
12
19
  const IMAGE_EXT_RE = /\.(jpe?g|png|webp|avif)$/i;
13
20
  /**
14
21
  * Zero image processing Vite plugin.
@@ -43,8 +50,11 @@ function imagePlugin(config = {}) {
43
50
  const defaultFormats = config.formats ?? ["webp"];
44
51
  const quality = config.quality ?? 80;
45
52
  const placeholderSize = config.placeholderSize ?? 16;
53
+ const placeholderStrategy = config.placeholder ?? "blur";
46
54
  const outSubDir = config.outDir ?? "assets/img";
47
55
  const include = config.include ?? IMAGE_EXT_RE;
56
+ const cdn = config.cdn;
57
+ const svgOpts = config.svg === true ? { currentColor: true } : config.svg === false || config.svg === void 0 ? false : config.svg;
48
58
  let root = "";
49
59
  let outDir = "";
50
60
  let isBuild = false;
@@ -57,13 +67,70 @@ function imagePlugin(config = {}) {
57
67
  isBuild = resolvedConfig.command === "build";
58
68
  },
59
69
  async resolveId(id) {
70
+ if (svgOpts && id.includes("?component") && id.split("?")[0].endsWith(".svg")) return `\0virtual:zero-svg:${id}`;
60
71
  if (id.includes("?optimize") && include.test(id.split("?")[0])) return `\0virtual:zero-image:${id}`;
61
72
  return null;
62
73
  },
63
74
  async load(id) {
75
+ if (id.startsWith("\0virtual:zero-svg:")) {
76
+ const rawPath = id.replace("\0virtual:zero-svg:", "").split("?")[0] ?? id;
77
+ const absPath = rawPath.startsWith("/") ? join(root, rawPath) : rawPath;
78
+ if (!existsSync(absPath)) return null;
79
+ let svg = await readFile(absPath, "utf-8");
80
+ if (svgOpts && svgOpts.currentColor !== false) svg = svg.replace(/fill="(?!none)[^"]*"/g, "fill=\"currentColor\"").replace(/stroke="(?!none)[^"]*"/g, "stroke=\"currentColor\"");
81
+ const defaultSize = svgOpts && svgOpts.defaultSize;
82
+ if (defaultSize && !svg.includes("width=")) svg = svg.replace("<svg", `<svg width="${defaultSize}" height="${defaultSize}"`);
83
+ return `
84
+ import { h } from '@pyreon/core'
85
+ const _svg = ${JSON.stringify(svg)}
86
+ export default function SvgComponent(props) {
87
+ const el = h('span', {
88
+ ...props,
89
+ dangerouslySetInnerHTML: { __html: _svg },
90
+ style: [
91
+ 'display:inline-flex;align-items:center;justify-content:center',
92
+ props.width ? 'width:' + props.width + 'px' : '',
93
+ props.height ? 'height:' + props.height + 'px' : '',
94
+ props.style || '',
95
+ ].filter(Boolean).join(';'),
96
+ })
97
+ return el
98
+ }
99
+ `;
100
+ }
64
101
  if (!id.startsWith("\0virtual:zero-image:")) return null;
65
102
  const rawPath = id.replace("\0virtual:zero-image:", "").split("?")[0] ?? id;
66
103
  const absPath = rawPath.startsWith("/") ? join(root, "public", rawPath) : rawPath;
104
+ if (cdn) {
105
+ const metadata = await getImageMetadata(absPath);
106
+ const sources = defaultWidths.map((w) => ({
107
+ src: cdn(rawPath, {
108
+ width: w,
109
+ quality,
110
+ format: defaultFormats[0]
111
+ }) ?? rawPath,
112
+ width: w,
113
+ format: defaultFormats[0]
114
+ }));
115
+ const srcset = sources.map((s) => `${s.src} ${s.width}w`).join(", ");
116
+ const result = {
117
+ src: sources[sources.length - 1]?.src ?? rawPath,
118
+ srcset,
119
+ width: metadata.width,
120
+ height: metadata.height,
121
+ placeholder: placeholderStrategy === "none" ? "" : await generateBlurPlaceholder(absPath, placeholderSize),
122
+ formats: defaultFormats.map((fmt) => ({
123
+ type: `image/${fmt}`,
124
+ srcset: defaultWidths.map((w) => `${cdn(rawPath, {
125
+ width: w,
126
+ quality,
127
+ format: fmt
128
+ }) ?? rawPath} ${w}w`).join(", ")
129
+ })),
130
+ sources
131
+ };
132
+ return `export default ${JSON.stringify(result)}`;
133
+ }
67
134
  if (!isBuild) {
68
135
  const result = await loadDevImage(absPath, rawPath, placeholderSize);
69
136
  return `export default ${JSON.stringify(result)}`;
@@ -285,5 +352,5 @@ async function generateBlurPlaceholder(input, size) {
285
352
  }
286
353
 
287
354
  //#endregion
288
- export { imagePlugin, parseJpegDimensions, parseWebPDimensions };
355
+ export { cdnProviders, imagePlugin, parseJpegDimensions, parseWebPDimensions };
289
356
  //# sourceMappingURL=image-plugin.js.map