astro-icon
Version:
This **[Astro integration](https://docs.astro.build/en/guides/integrations-guide/)** provides a straight-forward `Icon` component for [Astro](https://astro.build).
123 lines (121 loc) • 4.89 kB
JavaScript
import { createHash } from "node:crypto";
import { parse, resolve } from "node:path";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import loadLocalCollection from "./loaders/loadLocalCollection.js";
import loadIconifyCollections from "./loaders/loadIconifyCollections.js";
export function createPlugin({ include = {}, iconDir = "src/icons", svgoOptions }, ctx) {
let collections;
const { root } = ctx;
const virtualModuleId = "virtual:astro-icon";
const resolvedVirtualModuleId = "\0" + virtualModuleId;
return {
name: "astro-icon",
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
async load(id) {
if (id === resolvedVirtualModuleId) {
try {
if (!collections) {
collections = await loadIconifyCollections({ root, include });
}
const local = await loadLocalCollection(iconDir, svgoOptions);
collections["local"] = local;
logCollections(collections, { ...ctx, iconDir });
await generateIconTypeDefinitions(Object.values(collections), root);
}
catch (ex) {
// Failed to load the local collection
}
return `export default ${JSON.stringify(collections)};\nexport const config = ${JSON.stringify({ include })}`;
}
},
configureServer({ watcher, moduleGraph }) {
watcher.add(`${iconDir}/**/*.svg`);
watcher.on("all", async (_, filepath) => {
const parsedPath = parse(filepath);
const resolvedIconDir = resolve(root.pathname, iconDir);
const isSvgFileInIconDir = parsedPath.dir.startsWith(resolvedIconDir) &&
parsedPath.ext === ".svg";
const isAstroConfig = parsedPath.name === "astro.config";
if (!isSvgFileInIconDir && !isAstroConfig)
return;
console.log(`Local icons changed, reloading`);
try {
if (!collections) {
collections = await loadIconifyCollections({ root, include });
}
const local = await loadLocalCollection(iconDir, svgoOptions);
collections["local"] = local;
logCollections(collections, { ...ctx, iconDir });
await generateIconTypeDefinitions(Object.values(collections), root);
moduleGraph.invalidateAll();
}
catch (ex) {
// Failed to load the local collection
}
return `export default ${JSON.stringify(collections)};\nexport const config = ${JSON.stringify({ include })}`;
});
},
};
}
function logCollections(collections, { logger, iconDir }) {
if (Object.keys(collections).length === 0) {
logger.warn("No icons detected!");
return;
}
const names = Object.keys(collections).filter((v) => v !== "local");
if (collections["local"]) {
names.unshift(iconDir);
}
logger.info(`Loaded icons from ${names.join(", ")}`);
}
async function generateIconTypeDefinitions(collections, rootDir, defaultPack = "local") {
const typeFile = new URL("./.astro/icon.d.ts", rootDir);
await ensureDir(new URL("./", typeFile));
const oldHash = await tryGetHash(typeFile);
const currentHash = collectionsHash(collections);
if (currentHash === oldHash) {
return;
}
await writeFile(typeFile, `// Automatically generated by astro-icon
// ${currentHash}
declare module 'virtual:astro-icon' {
\texport type Icon = ${collections.length > 0
? collections
.map((collection) => Object.keys(collection.icons)
.concat(Object.keys(collection.aliases ?? {}))
.map((icon) => `\n\t\t| "${collection.prefix === defaultPack
? ""
: `${collection.prefix}:`}${icon}"`))
.flat(1)
.join("")
: "never"};
}`);
}
function collectionsHash(collections) {
const hash = createHash("sha256");
for (const collection of collections) {
hash.update(collection.prefix);
hash.update(Object.keys(collection.icons)
.concat(Object.keys(collection.aliases ?? {}))
.sort()
.join(","));
}
return hash.digest("hex");
}
async function tryGetHash(path) {
try {
const text = await readFile(path, { encoding: "utf-8" });
return text.split("\n", 3)[1].replace("// ", "");
}
catch { }
}
async function ensureDir(path) {
try {
await mkdir(path, { recursive: true });
}
catch { }
}