Files
budget2.0/scripts/fetch-fonts.ts
T
Lucas WintherandClaude Opus 5 d83edccc79 docs: move the long-form docs into docs/, without the superpowers nesting
ARCHITECTURE, PRODUCT and TODO move out of the repository root, and
docs/superpowers/{specs,plans} flattens to docs/{specs,plans}:
"superpowers" names the tool that wrote those documents, not a kind of
document.

No content changes. This commit is the move, plus the roughly thirty
cross-references the move invalidated.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-02 21:56:59 +02:00

64 lines
2.5 KiB
TypeScript
Executable File

/**
* Downloads the self-hosted webfonts into src/fonts/.
*
* bun scripts/fetch-fonts.ts
*
* Self-hosted, not linked from a CDN, because docs/PRODUCT.md's position is that the
* app works offline and needs no third party: a font request to another origin
* on every cold load contradicts that, and would leak the fact that the app was
* opened. The files are COMMITTED so neither the build nor a fresh checkout
* needs network.
*
* Only the `latin` and `latin-ext` subsets are taken. Danish needs æ, ø and å
* (U+00E6, U+00F8, U+00E5), all inside `latin`; `latin-ext` covers the rest of
* European diacritics for post names. Cyrillic, Greek and Vietnamese are
* dropped, which is most of the weight.
*
* Both faces are SIL Open Font License 1.1, which permits self-hosting as long
* as the licence travels with them — hence src/fonts/OFL.txt.
*/
import { mkdir } from "node:fs/promises";
const UA =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36";
const FAMILIES = [
{ css: "Inter:[email protected]", slug: "inter" },
{ css: "JetBrains+Mono:[email protected]", slug: "jetbrains-mono" },
];
const WANTED = new Set(["latin", "latin-ext"]);
await mkdir("src/fonts", { recursive: true });
for (const family of FAMILIES) {
const url = `https://fonts.googleapis.com/css2?family=${family.css}&display=swap`;
const css = await (await fetch(url, { headers: { "user-agent": UA } })).text();
// Google labels each @font-face block with a /* subset */ comment above it.
const blocks = css.split("/*").slice(1);
let taken = 0;
for (const block of blocks) {
const subset = block.slice(0, block.indexOf("*/")).trim();
if (!WANTED.has(subset)) continue;
const src = /url\((https:\/\/[^)]+\.woff2)\)/.exec(block)?.[1];
const range = /unicode-range:\s*([^;]+);/.exec(block)?.[1]?.trim();
if (!src || !range) throw new Error(`${family.slug}/${subset}: no src or unicode-range`);
const response = await fetch(src, { headers: { "user-agent": UA } });
if (!response.ok) throw new Error(`${src} returned ${response.status}`);
const bytes = new Uint8Array(await response.arrayBuffer());
const file = `src/fonts/${family.slug}-${subset}.woff2`;
await Bun.write(file, bytes);
console.log(`${file} ${bytes.length} bytes`);
console.log(` unicode-range: ${range}`);
taken++;
}
if (taken !== WANTED.size) {
throw new Error(`${family.slug}: took ${taken} subsets, expected ${WANTED.size}`);
}
}