Last active
November 3, 2024 19:28
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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