fontfetch (CLI + Core)
Paste a URL, get every webfont — extracted, license-classified, and project-ready. One npm install, zero runtime deps.
Summary
fontfetch is a published Node CLI (`npx fontfetch <url>`) and a shared TypeScript pipeline library (`@fontfetch/core`). It walks every stylesheet a browser would, parses every `@font-face`, downloads the binaries into a provenance-bucketed folder (`google/`, `adobe-typekit/`, `commercial/`, `open-cdn/`, `self-hosted/`), classifies each face as open / commercial / unknown, and emits a ready-to-paste `fonts.css` plus framework configs (Next.js `next/font/local`, Tailwind `fontFamily`, Vite guide). Three subcommands ship together — `pull` (default), `inspect` (terminal-native font report via fontkit), and `subset` (Playwright DOM walk + harfbuzzjs WASM subset, no Python `fonttools` install). Released on npm with Trusted Publishing (OIDC), CI across Linux/macOS/Windows × Node 18/20/22, and 101 passing vitest cases.
Target user
Web designers and front-end engineers prototyping with fonts they've seen in the wild — plus brand teams auditing what typography a live site actually serves. The CLI runs locally; no account, no telemetry.
- 01
Shipped a zero-runtime-dep CLI on npm — `npx fontfetch <url>` works out of the box with no install step, Playwright and harfbuzzjs are lazy-loaded optional peers.
- 02
Built `fontfetch inspect <file>` — terminal-native Wakamai Fondue equivalent printing glyph count, units-per-em, variation axes, OpenType features, name-table records, vendor, copyright, and SIL OFL detection with Reserved Font Name (RFN) clause awareness.
- 03
Built `--fallback` — capsize-driven `size-adjust` / `ascent-override` / `descent-override` / `line-gap-override` blocks matched to a system fallback per family.
Framework-agnostic CLS killer that works in any CSS surface (unlike `fontaine` which is Nuxt/Vite-only).
- 04
Built `fontfetch subset <url>` — runs the full extraction, then loads the page in headless Chromium, walks every visible text node plus `::before`/`::after` `content`, and subsets each font to actually-rendered codepoints. Pure-Node via harfbuzzjs WASM, no Python `fonttools` dependency (unlike `glyphhanger`).
- 05
Shipped a heuristic license classifier — URL-signature match against Adobe Typekit / Monotype `fast.fonts.net` / Hoefler `cloud.typography.com` / Type Network plus a curated SIL OFL / Google Fonts family-name fallback catalog. Fail-fast aborts on all-commercial pages and writes `LICENSE_REVIEW.md` only; `--force` to override.
- 06
Built three framework emitters — `next.fonts.ts` (one `localFont` call per family with all weights), `tailwind.fonts.ts` (sans/serif/mono heuristic + per-family aliases), and a `vite.fonts.md` integration guide. Pluggable Emitter interface; add a new target in one file.
- 07
Set up FAANG-grade open-core release infrastructure — manual `workflow_dispatch` release workflow with dry-run mode, version-bump guard, npm Trusted Publishing via OIDC (no long-lived publish token in the repo), automatic git tag + GitHub release on success.
- 08
Designed a structured `PullProgressEvent` union and optional `onProgress` callback in `pull()` so non-CLI consumers can stream the pipeline live — same library powers a future webapp without any parallel codepaths.
Early users hit `1 unique file(s)` and assumed the rest were missing — actually one variable binary covered the whole family. Solved by inspecting every downloaded binary post-pull via fontkit, detecting any with variation axes, and printing a one-liner with the axis ranges (`wght 300..900, ital 0..10`). Non-fatal: parse failures are swallowed because the download itself already succeeded.
The CLI imports from `@fontfetch/core` (a private workspace package), but npm consumers can't resolve `workspace:*`. Solved with `tsup`'s `noExternal: ['@fontfetch/core']` — core is inlined into the published `dist/cli.js`, so the tarball is a single self-contained file. Playwright stays an external optional peer so the static path runs with zero runtime deps.
- L01
Heuristic classifiers should bias toward the safe failure mode.
The license classifier prefers false-commercial over false-open — a user shipping a commercial face they thought was OFL has a legal problem; a user told their OFL face is commercial just runs `--force` and moves on.
- L02
Framework-agnostic beats framework-native when the underlying capability is identical.
`--fallback` works in any CSS surface precisely because I resisted the urge to ship a Vite plugin or a next.config.js patcher. Each framework integration would be a smaller audience; the capability is the wedge.
- L03
Optional peer dependencies are how you keep a zero-install CLI feel while shipping power features.
Playwright and `subset-font` are both opt-in — the help text tells you exactly what to install if you hit the path that needs them, and the static pipeline runs with nothing but Node.
- versions shipped
- 14
- vitest cases passing
- 101/101
- source loc total
- ~3600
- source loc lib
- ~2740
- test loc
- ~860
- framework emitters
- 3
- subcommands
- 3
- ci matrix
- 3 OS × 3 Node = 9 build configs