monkeytype/frontend/vite-plugins/fontawesome-subset.ts

207 lines
5.2 KiB
TypeScript

import { Plugin } from "vite";
import * as fs from "fs";
import { createRequire } from "module";
import * as path from "path";
import { fontawesomeSubset as createFontawesomeSubset } from "fontawesome-subset";
/**
* Detect fontawesome icons used by the application and creates subset font files only containing the used icons.
* @param options
* @returns
*/
export function fontawesomeSubset(): Plugin {
return {
name: "vite-plugin-fontawesome-subset",
apply: "build",
async buildStart() {
const start = performance.now();
console.log("\nCreating fontawesome subset...");
const fontawesomeClasses = getFontawesomeConfig();
await createFontawesomeSubset(
fontawesomeClasses,
"src/webfonts-generated",
{
targetFormats: ["woff2"],
},
);
const end = performance.now();
console.log(
`Creating fontawesome subset took ${Math.round(end - start)} ms`,
);
},
};
}
type FontawesomeConfig = {
/* used regular icons without `fa-` prefix*/
regular: string[];
/* used solid icons without `fa-` prefix*/
solid: string[];
/* used brands icons without `fa-` prefix*/
brands: string[];
};
type FileObject = { name: string; isDirectory: boolean };
const iconSet = {
solid: parseIcons("solid"),
regular: parseIcons("regular"),
brands: parseIcons("brands"),
};
/**
* Map containing reserved classes by module
*/
const modules2 = {
animated: ["spin", "pulse"],
"bordererd-pulled": ["border", "pull-left", "pull-right"],
"fixed-width": ["fw"],
larger: [
"lg",
"xs",
"sm",
"1x",
"2x",
"3x",
"4x",
"5x",
"6x",
"7x",
"8x",
"9x",
"10x",
],
"rotated-flipped": [
"rotate-90",
"rotate-180",
"rotate-270",
"flip-horizontal",
"flip-vertical",
"flip-both",
],
stacked: ["stack", "stack-1x", "stack-2x", "inverse"],
};
/**
* Detect used fontawesome icons in the directories `src/**` and `static/**{.html|.css}`
* @param {boolean} debug - Enable debug output
* @returns {FontawesomeConfig} - used icons
*/
function getFontawesomeConfig(debug = false): FontawesomeConfig {
const time = Date.now();
const srcFiles = findAllFiles(
"./src",
(filename) =>
!filename.endsWith("fontawesome-5.scss") &&
!filename.endsWith("fontawesome-6.scss"), //ignore our own css
);
const staticFiles = findAllFiles(
"./static",
(filename) => filename.endsWith(".html") || filename.endsWith(".css"),
);
const allFiles = [...srcFiles, ...staticFiles];
const usedClassesSet: Set<string> = new Set();
const regex = /\bfa-[a-z0-9-]+\b/g;
for (const file of allFiles) {
const fileContent = fs.readFileSync("./" + file).toString();
const matches = fileContent.match(regex);
if (matches) {
matches.forEach((match) => {
const [icon] = match.split(" ");
usedClassesSet.add((icon as string).substring(3));
});
}
}
const usedClasses = [...usedClassesSet].sort();
const allModuleClasses = new Set(Object.values(modules2).flatMap((it) => it));
const icons = usedClasses.filter((it) => !allModuleClasses.has(it));
const solid = icons.filter((it) => iconSet.solid.includes(it));
const regular = icons.filter((it) => iconSet.regular.includes(it));
const brands = usedClasses.filter((it) => iconSet.brands.includes(it));
const leftOvers = icons.filter(
(it) =>
!(solid.includes(it) || regular.includes(it) || brands.includes(it)),
);
if (leftOvers.length !== 0) {
throw new Error(
"Fontawesome failed with unknown icons: " + leftOvers.toString(),
);
}
if (debug) {
console.debug(
"Make sure fontawesome modules are active: ",
Object.entries(modules2)
.filter((it) => usedClasses.filter((c) => it[1].includes(c)).length > 0)
.map((it) => it[0])
.filter((it) => it !== "brands")
.join(", "),
);
console.debug(
"Here is your config: \n",
JSON.stringify({
regular,
solid,
brands,
}),
);
console.debug("Detected fontawesome classes in", Date.now() - time, "ms");
}
return {
regular,
solid,
brands,
};
}
//detect if we run this as a main
if (import.meta.url.endsWith(process.argv[1] as string)) {
getFontawesomeConfig(true);
}
function toFileAndDir(dir: string, file: string): FileObject {
const name = path.join(dir, file);
return { name, isDirectory: fs.statSync(name).isDirectory() };
}
function findAllFiles(
dir: string,
filter: (filename: string) => boolean = (_it): boolean => true,
): string[] {
const files = fs
.readdirSync(dir)
.map((it) => toFileAndDir(dir, it))
.filter((file) => file.isDirectory || filter(file.name));
const out: string[] = [];
for (const file of files) {
if (file.isDirectory) {
out.push(...findAllFiles(file.name, filter));
} else {
out.push(file.name);
}
}
return out;
}
function parseIcons(iconSet: string): string[] {
const require = createRequire(import.meta.url);
const path = require.resolve(
`@fortawesome/fontawesome-free/js/${iconSet}.js`,
);
const file: string | null = fs.readFileSync(path).toString();
return file
?.match(/"(.*)": \[.*\],/g)
?.map((it) => it.substring(1, it.indexOf(":") - 1)) as string[];
}