Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
5.0 kB · 134 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135/** * The stylesheet and src/fonts/ have to agree, and there are two ways they stop. * * A face can name a file that is not there. Vite fails the build on that one, * loudly, so it is not the interesting case — but it is free to check here and * it fails in a second rather than after a bundle. * * The other way is quiet, and it is why this file exists. A weight is added to * a rule and no face supplies it, so the browser synthesises one: a fake bold * that is the regular smeared sideways, or a fake light that is the regular. It * renders, it looks nearly right, and nothing anywhere says the file is * missing. So this reads every font-weight the stylesheet asks for, works out * which family the rule resolves to, and checks a real face covers it. * * The same reading catches the opposite: a file nobody declares, left behind * when a weight was dropped from a rule. Those are 20KB each in the bundle and * there is nothing to notice them by. * * Run with `npm test`. */import { test } from "node:test";import assert from "node:assert/strict";import { readdir, readFile } from "node:fs/promises";import { fileURLToPath } from "node:url";
const cssUrl = new URL("../src/styles.css", import.meta.url);// Comments out first: this file's are long and full of commas and braces, and// every rule below is found by matching on those.const css = (await readFile(fileURLToPath(cssUrl), "utf8")).replace( /\/\*[\s\S]*?\*\//g, "",);const fontsDir = fileURLToPath(new URL("../src/fonts/", import.meta.url));const files = (await readdir(fontsDir)).filter((f) => f.endsWith(".woff2"));
/** Every @font-face in the stylesheet, as {family, weights, file}. */const faces = [...css.matchAll(/@font-face\s*\{([^}]*)\}/g)].map( ([, block]) => { const field = (name) => block.match(new RegExp(`${name}:\\s*([^;]+);`))?.[1].trim(); const weight = field("font-weight") ?? "400"; // "400 700" is a variable range; a single value is a static instance. const [lo, hi = lo] = weight.split(/\s+/).map(Number); return { family: field("font-family")?.replace(/["']/g, ""), lo, hi, file: block.match(/url\("\.\/fonts\/([^"]+)"\)/)?.[1], }; },);
/** The families the type tokens point at, keyed by token name. */const tokens = new Map( [...css.matchAll(/--font-([a-z]+):\s*([^;]+);/g)].map(([, name, stack]) => [ `--font-${name}`, stack.split(",")[0].trim().replace(/["']/g, ""), ]),);
test("every face names a file that exists", () => { const missing = faces.filter((f) => !f.file || !files.includes(f.file)); assert.deepEqual( missing.map((f) => `${f.family} ${f.lo}: ${f.file ?? "no url"}`), [], "an @font-face points at a file that is not in src/fonts/", );});
test("every file is declared by a face", () => { const declared = new Set(faces.map((f) => f.file)); const orphans = files.filter((f) => !declared.has(f)); assert.deepEqual( orphans, [], "these files ship in the bundle and no @font-face asks for them", );});
test("every type token resolves to a declared family", () => { const families = new Set(faces.map((f) => f.family)); const dangling = [...tokens].filter(([, family]) => !families.has(family)); assert.deepEqual( dangling.map(([token, family]) => `${token}: ${family}`), [], "a token names a family with no @font-face, so it silently falls back", );});
/** * Which token a selector's family comes from, by reading the rule that sets it. * Anything a rule does not name inherits body's, which is --font-text. */function familyOf(selector) { for (const [, sel, body] of css.matchAll(/([^{}]+)\{([^}]*)\}/g)) { if (!body.includes("font-family: var(--font-")) continue; const selectors = sel.split(",").map((s) => s.trim()); if (!selectors.includes(selector)) continue; const token = body.match(/font-family: var\((--font-[a-z]+)\)/)[1]; return tokens.get(token); } return tokens.get("--font-text");}
test("no rule asks for a weight no face supplies", () => { // The selector a declaration sits under, so a weight can be traced to a family. const rules = [...css.matchAll(/([^{}]+)\{([^}]*)\}/g)] .filter(([, sel]) => !sel.includes("@font-face")) .flatMap(([, sel, body]) => { const weight = body.match(/font-weight:\s*(\d+)\s*;/); if (!weight) return []; return sel .split(",") .map((s) => s.trim()) .filter(Boolean) .map((s) => ({ selector: s, weight: Number(weight[1]) })); });
const synthesised = []; for (const { selector, weight } of rules) { const family = familyOf(selector); const covered = faces.some( (f) => f.family === family && weight >= f.lo && weight <= f.hi, ); if (!covered) synthesised.push(`${selector} wants ${family} ${weight}`); }
assert.deepEqual( synthesised, [], "the browser will synthesise these rather than say anything:\n " + synthesised.join("\n "), );});