UNPKG

astro-favicons

Version:

An all-in-one favicon and PWA assets generator for Astro projects. It automates the creation of favicons, manifest, and supports hot reloading for efficient development. Powered by `astro-capo`, it keeps your ๏นค๐š‘๐šŽ๐šŠ๐š๏นฅ content well-organized and tidy.

243 lines (233 loc) โ€ข 6.72 kB
import favicons from 'favilib'; import path from 'path'; const defaultSource = "public/favicon.svg"; const defaults = { name: null, short_name: null, icons: { android: [ "android-chrome-192x192.png", { name: "android-chrome-512x512.png", sizes: [{ width: 512, height: 512 }], purpose: "maskable", transparent: true, rotate: false, offset: 13 } ], appleIcon: [ "apple-touch-icon.png", "apple-touch-icon-precomposed.png", "safari-pinned-tab.svg" ], appleStartup: false, favicons: true, windows: true, yandex: true } }; const name = "astro-favicons"; async function getIconsForPlatform(input, params) { const { options, platform } = params; const offOptions = Object.fromEntries( Object.entries(options.icons).map(([key, value]) => [ key, key === platform ? value : false ]) ); return await favicons(input, { ...options, icons: offOptions }); } function mergeResults(results) { return results.reduce( (acc, { platform, response: res }) => { acc.images.push(...res.images.map((image) => ({ ...image, platform }))); acc.files.push(...res.files.map((file) => ({ ...file, platform }))); acc.html.push(...res.html); return acc; }, { images: [], files: [], html: [] } ); } async function fetch(input, options) { const platforms = Object.keys(options.icons); const results = await Promise.all( platforms.map(async (platform) => ({ platform, response: await getIconsForPlatform(input?.[platform], { platform, options }) })) ); return mergeResults(results); } function isSource(value) { return typeof value === "string" || Buffer.isBuffer(value) || Array.isArray(value) && value.every((item) => typeof item === "string" || Buffer.isBuffer(item)); } function getInput(opts) { let input = opts?.input; const icons = Object.fromEntries( Object.keys(opts?.icons || {}).map((key) => [key, defaultSource]) ); if (!input) { return icons; } if (isSource(input)) { return Object.fromEntries( Object.keys(icons).map((key) => [key, input]) ); } const unionSource = [ ...new Set( Object.entries(input).filter(([_, value]) => value !== void 0).map(([_, value]) => value).flat() ) ]; const result = {}; for (const key of Object.keys(icons)) { result[key] = input[key] !== void 0 ? input[key] : unionSource.length === 1 ? unionSource[0] : unionSource; } return result; } function normalizePath(prefix) { if (!prefix) return ""; const regex = /^\/+|\/+$/g; try { const url = new URL(prefix); return url.pathname.replace(regex, "") + "/"; } catch { return prefix.replace(regex, "") + "/"; } } function mime(fileName) { const ext = path.extname(fileName).toLowerCase().slice(1); const contentTypes = { ico: "image/x-icon", svg: "image/svg+xml", png: "image/png", webp: "image/webp", jpg: "image/jpeg", jpeg: "image/jpeg", json: "application/json", xml: "application/xml", webmanifest: "application/manifest+json" }; return contentTypes[ext] || "application/octet-stream"; } const Styles = { Reset: "\x1B[0m", Dim: "\x1B[2m", FgGreen: "\x1B[32m", FgYellow: "\x1B[33m", FgBlue: "\x1B[34m" }; function styler(message, styles = ["Reset"]) { const appliedStyles = styles.map((style) => Styles[style] || Styles.Reset).join(""); return `${appliedStyles}${message}${Styles.Reset}`; } function formatTime(duration) { return duration < 1e3 ? `${duration.toFixed()}ms` : `${(duration / 1e3).toFixed(2)}s`; } async function handleAssets(opts, params) { const virtualModuleId = `virtual:${name}`; const resolvedVirtualModuleId = "\0" + virtualModuleId; let sources = getInput(opts); const startAt = performance.now(); const { images, files, html } = await fetch(sources, opts); const processedTime = performance.now() - startAt; const { isRestart, logger } = params; let base = normalizePath(opts.output?.assetsPrefix); logger.info( `${files.length} file(s), ${images.length} image(s)` + styler( `${!isRestart ? " \u2713 Completed in" : ""} ${formatTime(processedTime)}.`, [`${!isRestart ? "FgGreen" : "Dim"}`] ) ); return { name, enforce: "pre", resolveId(id) { if (id === virtualModuleId) { return resolvedVirtualModuleId; } }, load(id) { if (id === resolvedVirtualModuleId) { return `export const html = ${JSON.stringify(html)}; export const opts = ${JSON.stringify(opts)}`; } }, configureServer(server) { server.middlewares.use(async (req, res, next) => { try { const reqUrl = decodeURIComponent(req.url || ""); const fileName = reqUrl.split("?")[0].split("/").pop(); const resource = images.find((img) => img.name === fileName) || files.find((file) => file.name === fileName); if (resource && req.url?.startsWith(`/${base}${resource?.name}`)) { const mimeType = mime(fileName); res.setHeader("Content-Type", mimeType); res.setHeader("Cache-Control", "no-cache"); res.end(resource.contents); return; } next(); } catch (err) { console.error("Error in middleware:", err); res.statusCode = 500; res.end("Internal Server Error"); } }); }, generateBundle() { try { const emitFile = (file) => { const fileId = this.emitFile({ type: "asset", fileName: base + file.name, source: file.contents }); }; images.forEach((image) => emitFile(image)); files.forEach((file) => emitFile(file)); } catch (err) { throw err; } } }; } function createIntegration(options) { const opts = { ...defaults, ...options }; return { name, hooks: { "astro:config:setup": async ({ config, isRestart, command: cmd, updateConfig, logger, addMiddleware }) => { opts.withCapo = opts.withCapo ?? config.compressHTML; if (cmd === "build" || cmd === "dev") { if (!isRestart) { logger.info(`Processing source...`); } updateConfig({ vite: { plugins: [await handleAssets(opts, { isRestart, logger })] } }); } addMiddleware({ entrypoint: `${name}/middleware`, order: "pre" }); } } }; } export { createIntegration as default };