Files

65 lines
3.2 KiB
JavaScript
Raw Permalink Normal View History

// The icon fonts are subset to what this interface draws: 82 of Font Awesome's 2468,
// which took 299 KB of woff2 down to 12 KB. The saving comes with a failure mode that
// is completely silent — add an icon to a component and it renders as an empty box,
// with no error anywhere, because the glyph is simply not in the shipped font.
//
// So the manifest scripts/subset-icons.py writes is checked against the source here.
// A new icon fails the build with the name of the icon and the command to run.
import assert from 'node:assert/strict';
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const read = (rel) => readFileSync(path.join(ROOT, rel), 'utf8');
const manifest = JSON.parse(read('assets/fontawesome/subset-icons.json'));
const covered = new Set(manifest.icons);
const notIcons = new Set(manifest.notIcons);
// Every fa- class the interface names, from the same files the subsetter reads.
function sourceFiles(dir) {
const out = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) out.push(...sourceFiles(full));
else if (/\.(js|jsx|css|html)$/.test(entry.name)) out.push(full);
}
return out;
}
const used = new Set();
for (const file of [...sourceFiles(path.join(ROOT, 'src')), path.join(ROOT, 'index.html')]) {
for (const match of readFileSync(file, 'utf8').matchAll(/\bfa-([a-z0-9-]+)/g)) {
if (!notIcons.has(match[1])) used.add(match[1]);
}
}
const missing = [...used].filter((name) => !covered.has(name)).sort();
assert.deepEqual(missing, [],
`these icons are used but not in the subset, so they render as empty boxes: ${missing.join(', ')}\n` +
' fix: pip install fonttools brotli && python3 scripts/subset-icons.py && npm run build');
// The subset fonts and the stylesheet that names them have to exist and be small — the
// point of the exercise is the size, and a regenerate that quietly fell back to the full
// font would still pass every assertion above.
const bundle = read('assets/app.css');
for (const family of ['fa-solid-900', 'fa-regular-400', 'fa-brands-400']) {
const rel = `assets/fontawesome/webfonts/${family}.subset.woff2`;
assert.ok(existsSync(path.join(ROOT, rel)), `${rel} is missing — run scripts/subset-icons.py`);
const size = statSync(path.join(ROOT, rel)).size;
assert.ok(size < 40_000, `${rel} is ${size} B — that is the full font, not a subset`);
assert.ok(bundle.includes(`${family}.subset.woff2`), `assets/app.css does not use the ${family} subset`);
}
// And the full stylesheet must not come back: it is 102 KB for icons that are not here.
assert.equal(bundle.includes('/assets/fontawesome/css/all.min.css'), false,
'the full Font Awesome stylesheet is being loaded again alongside the subset');
for (const shell of ['index.html', 'ru/index.html']) {
assert.equal(read(shell).includes('fontawesome/css/all.min.css'), false,
`${shell} still links the full Font Awesome stylesheet`);
}
console.log(`icon-subset.test.mjs: ${used.size} icons used, all covered by the subset`);