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.
242 lines (232 loc) โข 6.71 kB
JavaScript
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 };