Skip to content

Instantly share code, notes, and snippets.

@janispritzkau
Last active November 3, 2024 19:28
Show Gist options
  • Save janispritzkau/98909a699061267aea5a3e62704a096d to your computer and use it in GitHub Desktop.
Save janispritzkau/98909a699061267aea5a3e62704a096d to your computer and use it in GitHub Desktop.
Material Symbols subset font plugin for Vite (reduces bundle size from megabytes to kilobytes)
import { readFile } from "node:fs/promises";
import { relative } from "node:path";
import { Minimatch } from "minimatch";
import subsetFont from "subset-font";
import { Plugin } from "vite";
export interface SubsetOptions {
codepointsPath: string;
iconsGlob: string;
}
export function subsetIcons(options: SubsetOptions): Plugin {
const glob = new Minimatch(options.iconsGlob);
const names = new Set<string>();
let codepoints: Record<string, string>;
let codepointRegex: RegExp;
let outputInfo = {
icons: { total: 0, used: 0 },
size: { original: 0, subset: 0 },
};
return {
name: "subset-fonts",
async configResolved(config) {
if (config.command != "build") return;
codepoints = Object.fromEntries(
(await readFile(options.codepointsPath, "utf-8"))
.trim()
.split("\n")
.map((line) => line.split(" ") as [string, string]),
);
codepointRegex = new RegExp(
String.raw`(?<=[\s>'"\`])(${Object.keys(codepoints).join("|")})(?=[\s<'"\`])`,
"g",
);
},
transform(code, id) {
if (id.includes("node_modules")) return;
const path = relative(process.cwd(), id);
if (!glob.match(path)) return;
for (const match of code.matchAll(codepointRegex)) {
const before = match.input[match.index - 1];
const after = match.input[match.index + match[0].length];
if (
(before == ">" && after != "<") ||
(before == "'" && after != "'") ||
(before == "`" && after != "`") ||
(before == '"' && after != '"')
) {
continue;
}
names.add(match[0]);
}
},
async generateBundle(options, bundle) {
for (const item of Object.values(bundle)) {
if (item.type != "asset" || !item.names.includes("material-symbols-outlined.woff2"))
continue;
const input = Buffer.from(item.source);
const output = await subsetFont(
input,
Array.from(names)
.map((name) => `${String.fromCodePoint(parseInt(codepoints[name], 16))}${name}`)
.join(""),
{
targetFormat: "woff2",
// @ts-expect-error outdated types
noLayoutClosure: true,
variationAxes: {
wght: { min: 300, max: 400 },
GRAD: { min: -25, max: 0 },
},
},
);
outputInfo = {
icons: { total: Object.keys(codepoints).length, used: names.size },
size: { original: input.length, subset: output.length },
};
item.source = output;
}
},
closeBundle() {
const { icons, size } = outputInfo;
console.log(`subset ${icons.used} of ${icons.total} icons`);
console.log(
`${((size.subset / size.original) * 100).toFixed(2)}% of the original size ` +
`(${(size.original / 1024 ** 2).toFixed(3)}M to ${(size.subset / 1024 ** 2).toFixed(3)}M)`,
);
},
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment