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.

192 lines (187 loc) โ€ข 5.82 kB
import { html, opts } from 'virtual:astro-favicons'; import { defineMiddleware, sequence } from 'astro/middleware'; import { renderSync, parse, ELEMENT_NODE } from 'ultrahtml'; /** * @author Nate Moore * @license [Apache](src/middleware/capo/LICENSE) * Adapted from https://github.com/rviscomi/capo.js/blob/a4d8902d300bc5207b3b588984a3b5e67bdc38b1/src/lib/rules.js * Further modified by Nate Moore for the `astro-capo` project. * * Original code by Rick Viscomi. */ function has(value) { return typeof value === "string"; } function is(a, b) { return a === b; } function any(a, b) { return has(a) && b.includes(a.toLowerCase()); } const ElementWeights = { META: 10, TITLE: 9, PRECONNECT: 8, ASYNC_SCRIPT: 7, IMPORT_STYLES: 6, SYNC_SCRIPT: 5, SYNC_STYLES: 4, PRELOAD: 3, DEFER_SCRIPT: 2, PREFETCH_PRERENDER: 1, OTHER: 0 }; const ElementDetectors = { META: isMeta, TITLE: isTitle, PRECONNECT: isPreconnect, DEFER_SCRIPT: isDeferScript, ASYNC_SCRIPT: isAsyncScript, IMPORT_STYLES: isImportStyles, SYNC_SCRIPT: isSyncScript, SYNC_STYLES: isSyncStyles, PRELOAD: isPreload, PREFETCH_PRERENDER: isPrefetchPrerender }; const META_HTTP_EQUIV_KEYWORDS = [ "accept-ch", "content-security-policy", "content-type", "default-style", "delegate-ch", "origin-trial", "x-dns-prefetch-control" ]; function isMeta(name, a) { if (name === "base") return true; if (name !== "meta") return false; return has(a.charset) || is(a.name, "viewport") || any(a["http-equiv"], META_HTTP_EQUIV_KEYWORDS); } function isTitle(name) { return name === "title"; } function isPreconnect(name, { rel }) { return name === "link" && is(rel, "preconnect"); } function isAsyncScript(name, { src, async }) { return name === "script" && has(src) && has(async); } function isImportStyles(name, a, children) { const importRe = /@import/; if (name === "style") { return importRe.test(children); } return false; } function isSyncScript(name, { src, defer, async, type = "" }) { if (name !== "script") return false; return !(has(src) && (has(defer) || has(async) || is(type, "module")) || type.includes("json")); } function isSyncStyles(name, { rel }) { if (name === "style") return true; return name === "link" && is(rel, "stylesheet"); } function isPreload(name, { rel }) { return name === "link" && any(rel, ["preload", "modulepreload"]); } function isDeferScript(name, { src, defer, async, type }) { if (name !== "script") return false; return has(src) && has(defer) || has(src) && is(type, "module") && !has(async); } function isPrefetchPrerender(name, { rel }) { return name === "link" && any(rel, ["prefetch", "dns-prefetch", "prerender"]); } function getWeight(element) { for (const [id, detector] of Object.entries(ElementDetectors)) { const children = element.name === "style" && element.children.length > 0 ? renderSync(element) : ""; if (detector(element.name, element.attributes, children)) { return ElementWeights[id]; } } return ElementWeights.OTHER; } function capo(html) { const { headHtml, beforeHead, afterHead } = extractHeadContent(html); if (!headHtml) return html; const headAst = parse(headHtml); const headNode = headAst.children.find( (node) => node.type === ELEMENT_NODE && node.name === "head" ); if (!headNode) return html; const updatedHead = processHead(headNode); const renderedHead = renderSync(updatedHead); return `${beforeHead}${renderedHead}${afterHead}`; } function extractHeadContent(html) { const headStart = html.indexOf("<head>"); const headEnd = html.indexOf("</head>") + "</head>".length; if (headStart === -1 || headEnd === -1) { return { headHtml: null, beforeHead: html, afterHead: "" }; } const beforeHead = html.slice(0, headStart); const headHtml = html.slice(headStart, headEnd); const afterHead = html.slice(headEnd); return { headHtml, beforeHead, afterHead }; } function processHead(head) { const weightedChildren = head.children.map((node) => { if (node.type === ELEMENT_NODE) { const weight = getWeight(node); return [weight, node]; } }).filter(Boolean); const sortedChildren = weightedChildren.sort((a, b) => b[0] - a[0]).map(([_, element]) => element); return { ...head, children: sortedChildren }; } const useLocaleName = (locale) => { if (!locale) return opts.name; const localized = opts.name_localized?.[locale]; if (!localized) return opts.name; return typeof localized === "string" ? localized : localized.value; }; const localizedHTML = (locale) => { const namePattern = /(name="(application-name|apple-mobile-web-app-title)")\scontent="[^"]*"/; const tags = html.map( (line) => line.replace(namePattern, `name="$2" content="${useLocaleName(locale)}"`) ).join("\n"); return tags; }; const withCapo = defineMiddleware(async (ctx, next) => { try { if (html.length === 0) throw "done"; const res = await next(); if (res.headers.get("X-Astro-Route-Type") !== "page") { return res; } const doc = await res.text(); const headIndex = doc.indexOf("</head>"); const htmlSet = new Set(html); const isInjected = [...htmlSet].some((line) => doc.includes(line)); if (headIndex === -1 || !opts.withCapo && isInjected) throw "done"; const document = `${doc.slice(0, headIndex)} ${!isInjected ? localizedHTML(ctx.currentLocale) : ""} ${doc.slice(headIndex)}`; return new Response(opts.withCapo ? capo(document) : document, { status: res.status, headers: res.headers }); } catch (e) { if (e !== "done") { console.error("Error in withCapo middleware:", e); } return next(); } }); const onRequest = sequence(withCapo); export { localizedHTML, onRequest };