fontfetch 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +121 -1
- package/README.md +115 -19
- package/dist/cli.js +19151 -17901
- package/dist/cli.js.map +1 -1
- package/package.json +14 -11
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,126 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.4.0] — 2026-05-29
|
|
10
|
+
|
|
11
|
+
The "distribution surface + competitor-gap closeouts" release. **Eight features ship in one minor — the four engine closeouts plus the four distribution channels.** Tag line: *"fontfetch 1.4: extract → audit → ship. With every page, every weight, every font signal you didn't know you needed — and now everywhere you already work."*
|
|
12
|
+
|
|
13
|
+
After v1.4 the CLI covers four new release-gate surfaces (`diff`, `audit`, `budget`, `--emit tokens`, `--gdpr-report`), surfaces cross-page font drift with `CONSISTENCY.md`, emits per-weight Capsize fallbacks (closing the fontaine #53 gap that's been open 3+ years), variable-font collapse hints, machine-readable `provenance.json`, and ships a typed `@fontfetch/registry` npm package + GitHub Action + Raycast extension + Homebrew tap.
|
|
14
|
+
|
|
15
|
+
### Added — distribution channels (v1.4.x folded in)
|
|
16
|
+
|
|
17
|
+
- **`@fontfetch/registry`** — new typed npm package providing autocomplete-grade access to the community pairings registry. `allPairings()`, `findByFamily()`, `freeAlternativesFor()`, `findByTag()`, `allTags()`, `allFamilies()`. Pairings baked from `pairings/*.json` at build time. Consumed by the Raycast extension and any downstream tooling (font pickers, design plugins, VS Code extensions, Figma plugins).
|
|
18
|
+
- **`fontfetch-action` GitHub Action** at [`extensions/github-action/`](./extensions/github-action). Wraps `fontfetch audit <url> --json`, posts a PR comment with the verdict + per-family budgets, exits non-zero on failure. Inputs: `url`, `max-kb`, `per-family-kb`, `no-commercial`, `comment`, `fontfetch-version`. Outputs: `passed`, `total-kb`, `families`, `report-json`.
|
|
19
|
+
- **Homebrew Formula** at [`extensions/homebrew/fontfetch.rb`](./extensions/homebrew/fontfetch.rb). Source-of-truth Formula ready to copy into a `homebrew-fontfetch` tap repo when ~500+ stars warrant the maintenance.
|
|
20
|
+
- **Raycast extension** at [`extensions/raycast/`](./extensions/raycast). Three commands: *Extract Fonts from URL* (CSS to clipboard), *Audit URL* (HUD verdict), *Search Font Pairings* (registry-backed search).
|
|
21
|
+
- **`--gdpr-report` flag** on the default pull command. Emits `GDPR.md` + `gdpr.json` listing every third-party font request with self-host remediation per family. Driven by the post-LG München I 20 O 1393/21 (2022) German court ruling on Google Fonts CDN. New public exports: `buildGdprReport`, `formatGdprMarkdown`, `GdprReport`, `GdprFinding`.
|
|
22
|
+
- **Variable-font collapse hint.** After the variable-font surfacing line, fontfetch now scans for families that ship both a variable binary AND ≥ 2 static weight files. Emits a one-liner per family with the byte saving. New public exports: `detectCollapseOpportunities`, `formatCollapseHint`, `CollapseOpportunity`. `PullResult.collapseOpportunities` carries the structured findings for non-CLI consumers.
|
|
23
|
+
|
|
24
|
+
### Added — engine work
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`fontfetch diff <urlA> <urlB>` — new subcommand.** Runs `pull()` on both URLs in parallel, emits a structured diff: added / removed / shared families, byte delta, commercial delta. Use for staging-vs-prod checks, rebrand detection, competitor watching. `--json` for CI.
|
|
29
|
+
```bash
|
|
30
|
+
fontfetch diff https://staging.acme.com https://acme.com
|
|
31
|
+
fontfetch diff https://staging.acme.com https://acme.com --json
|
|
32
|
+
```
|
|
33
|
+
Powered by a new public export `diffPulls(urlA, urlB, baseDir, options)` returning a stable `FontDiff` shape.
|
|
34
|
+
|
|
35
|
+
- **`fontfetch audit <url> [flags]` — new subcommand.** Drop-in CI command. Non-zero exit when any configured rule is violated. Flags:
|
|
36
|
+
- `--max-kb <N>` — total bundle byte budget
|
|
37
|
+
- `--per-family-kb <list>` — per-family budgets, e.g. `Inter:30,Geist:40`
|
|
38
|
+
- `--no-commercial` — fail if any face is classified commercial
|
|
39
|
+
- `--json` — machine-readable output
|
|
40
|
+
```bash
|
|
41
|
+
fontfetch audit https://acme.com --max-kb 200 --no-commercial
|
|
42
|
+
fontfetch audit https://acme.com --per-family-kb Inter:50 --json
|
|
43
|
+
```
|
|
44
|
+
Powered by a new public export `audit(url, baseDir, options)` returning a stable `AuditReport`.
|
|
45
|
+
|
|
46
|
+
- **`fontfetch budget <url> --max-kb N` — new subcommand.** Convenience around `audit` for the bundle-size dimension only. Same `--json` and non-zero-exit semantics as `audit`. Pairs with size-limit-style CI flows.
|
|
47
|
+
|
|
48
|
+
- **`--emit tokens` — W3C / DTCG design tokens emitter.** New target alongside `next` / `tailwind` / `vite`. Emits `fonts.tokens.json` with W3C Design Tokens Community Group ([tr.designtokens.org/format/](https://tr.designtokens.org/format/)) compatible token entries for every family + weight, plus a Tailwind-aligned size + line-height ladder. Consumed by Style Dictionary, Tokens Studio for Figma, Specify, and any tool that follows the DTCG draft.
|
|
49
|
+
```bash
|
|
50
|
+
fontfetch https://vercel.com --emit tokens
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- **Cross-page consistency report.** When `--pages > 1`, fontfetch now writes `CONSISTENCY.md` per pull listing shared-vs-divergent families across crawled pages. Surfaces the *"homepage uses Inter; /blog uses Tiempos; /pricing uses both"* problem that's been invisible since `--pages` shipped in v1.2.1. Zero competitors do this — none of them crawl multiple pages in the first place. New public exports: `computeConsistency`, `buildPageFaceMap`, `buildConsistencyReport`.
|
|
54
|
+
|
|
55
|
+
- **Per-weight Capsize fallback metrics.** `--fallback` now emits one `<Family> Fallback` block per (family, weight, style) tuple instead of one per family. Each block carries matching `font-weight` and `font-style` declarations so browsers select the right fallback per face. Beats `fontaine` on their core feature (fontaine #53 — open 3+ years). New public export `buildPerFaceFallbacks(filesDir, faces)`; the v1.2 `buildFallbacksForDir(filesDir)` remains available for direct callers that want family-wide fallback.
|
|
56
|
+
|
|
57
|
+
- **`provenance.json` per pull.** Stable, machine-readable schema (`schemaVersion: '1.0'`) carrying the v1.3.1-refined classifications + v0.6 provenance buckets + per-file byte sizes. Consumed by the new `audit` subcommand, the upcoming `fontfetch-action` GitHub Action, and any external CI / design-system tooling. The human-readable `LICENSE_REVIEW.md` is preserved unchanged. New public exports: `buildProvenanceJson()`, `ProvenanceReport`, `ProvenanceFaceEntry`, `ProvenanceFileEntry`.
|
|
58
|
+
|
|
59
|
+
- **`PullResult.consistency` and `PullResult.fileSizes`.** New optional fields surface cross-page consistency data and per-file byte counts to non-CLI consumers (the webapp, the audit/diff pipeline).
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **CLI dispatch gains three new subcommands** (`diff`, `audit`, `budget`). Existing dispatch (`inspect`, `subset`, default `pull`) is unchanged.
|
|
64
|
+
- **`PullOptions.emit`** accepts `'tokens'` alongside the existing targets. Existing callers are unaffected.
|
|
65
|
+
- **`pull()` per-source face extraction** is preserved as a parallel `facesPerSource` array so the consistency report can attribute faces back to their page-of-origin. The flattened `faces` array is unchanged externally.
|
|
66
|
+
|
|
67
|
+
### Notes
|
|
68
|
+
|
|
69
|
+
- No new runtime dependencies. All four features compose on top of fontkit + capsize + the existing pipeline.
|
|
70
|
+
- Bundle size unchanged at ~2.2 MB.
|
|
71
|
+
- The new public exports follow the same stability guarantee as the rest of `@fontfetch/core`: additive changes only within a minor; shape changes require a major bump.
|
|
72
|
+
- `audit` runs the full `pull()` under the hood — no second-pass dry-run mode. For CI flows that need only the audit verdict and not the bundle, use `--json` and discard `outDir` after parsing the report.
|
|
73
|
+
- Test surface grew from 144 → 207 vitest cases (engine: provenance-json 8, tokens emitter 7, consistency 10, diff 3, audit 8, fallback per-weight 3; channels: gdpr 9, collapse 7, registry 8). All green.
|
|
74
|
+
- The `extensions/` directory (GitHub Action, Raycast, Homebrew) is intentionally outside the pnpm workspace — each channel ships independently with its own toolchain.
|
|
75
|
+
|
|
76
|
+
## [1.3.1] — 2026-05-29
|
|
77
|
+
|
|
78
|
+
The "signal quality" point release. Two binary-driven refinements that close out the v1.2.x carryover queue: monospace detection now reads the `post` table instead of guessing from the family name, and the license classifier now cross-references the binary's OpenType `name` table before the final classification ships to disk. Plus, the OFL Reserved Font Name clause — the most-misunderstood OSS-font compliance pitfall — gets a first-class callout in `LICENSE_REVIEW.md`.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
- **`--fallback` reads `post.isFixedPitch` before falling back to the name regex.** Catches monospace families whose name doesn't say "mono" (Operator, PragmataPro, Comic Code, Berkeley Mono), so they get `Courier New` as their CLS fallback instead of `Arial`. Cheap — `fontkit` is already a runtime dep used by `inspect` and the variable-font summariser; the new path is one extra `create()` per family during `--fallback` computation.
|
|
82
|
+
- **License classifier cross-references the downloaded binary's `name` table (ids 13 + 14) after the URL-signature pass.** Conservative promotion only: `unknown` faces whose binary self-declares OFL flip to `open`; commercial classifications are never demoted (URL signature still wins); `open` classifications are preserved. Closes the v1.1 roadmap item that was queued for v1.2.x.
|
|
83
|
+
- **`LICENSE_REVIEW.md` now surfaces the OFL Reserved Font Name clause per family** when the binary's `name` table declares it. Worded as a callout (`⚠ OFL Reserved Font Name — do not redistribute modified copies under the name "<family>"`) so users don't accidentally violate the most-cited OFL compliance bug.
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
- New public export on `@fontfetch/core`: `crossRefLicenseFromBinaries(classified, filesDir) → ClassifiedFace[]` (from a new `license/binary-license.ts` module).
|
|
87
|
+
- `LicenseClassification` gains an optional `hasRFN?: boolean` field. Set by the cross-ref pass; consumed by `buildLicenseReview`.
|
|
88
|
+
- `InspectionReport` gains an `isFixedPitch: boolean` field. Read from `font.post?.isFixedPitch` (boolean or uint32 — both shapes are coerced).
|
|
89
|
+
- `pickGenericFallback(familyName, hint?)` accepts an optional `{ isFixedPitch?: boolean }` hint. When the hint forces `monospace`, the name regex is bypassed. The single-arg form remains supported.
|
|
90
|
+
|
|
91
|
+
### Notes
|
|
92
|
+
- No new runtime dependencies. The cross-ref pass reuses the existing `inspect()` helper; the fallback pass reuses the existing `fontkit` runtime dep.
|
|
93
|
+
- Bundle size unchanged at ~2.2 MB.
|
|
94
|
+
- The cross-ref pass is non-fatal: missing files, parse failures, and absent OpenType tables all degrade gracefully back to the URL-signature classification.
|
|
95
|
+
- Test surface grew from 132 → 144 vitest cases (new: `binary-license` with 6 cases, 3 new `pickGenericFallback` hint cases, 3 new `buildLicenseReview` RFN-callout cases). All green.
|
|
96
|
+
|
|
97
|
+
## [1.3.0] — 2026-05-28
|
|
98
|
+
|
|
99
|
+
Three additions that round out the subsetting pipeline: format allowlists, codepoint whitelists, and Google-Fonts-style per-language splitting. After v1.3, fontfetch covers URL → folder extraction, per-language splits, and modern-format emit in a single CLI with no Python dependency.
|
|
100
|
+
|
|
101
|
+
### Added
|
|
102
|
+
- **`--formats=<list>` on the default pull command.** Comma-separated allowlist of font formats to keep (one or more of `woff2`, `woff`, `ttf`, `otf`, `eot`). Each face's `src:` list is narrowed to matching sources; faces with zero surviving sources are dropped with a warning rather than emitted broken. Addresses a long-standing community ask for modern-format-only output. Default behaviour is unchanged — every format the upstream CSS provides is still kept when the flag is absent.
|
|
103
|
+
```bash
|
|
104
|
+
fontfetch https://shinobidata.com --formats=woff2 # modern-only output, halves bundle size
|
|
105
|
+
fontfetch https://acme.com --formats=woff2,woff # slight legacy reach
|
|
106
|
+
```
|
|
107
|
+
- **`--whitelist=<spec>` on the `subset` subcommand.** Extra codepoints to always include in the subset on top of the DOM-walk result. Accepts the canonical CSS `unicode-range` syntax (`U+00A0,U+20AC,U+0020-007F`) and the more developer-ergonomic `0x` shorthand (`0xA0,0x20AC`). Pairs cleanly with the existing `preserveRanges` option for whole-script preservation.
|
|
108
|
+
```bash
|
|
109
|
+
fontfetch subset https://stripe.com --whitelist=U+00A0,U+20AC
|
|
110
|
+
```
|
|
111
|
+
- **`--split-ranges` on the `subset` subcommand — Google-Fonts-style per-language emit.** For every downloaded font binary, fontfetch now opens it with `fontkit`, intersects its character set against the canonical Google Fonts buckets (`latin`, `latin-ext`, `cyrillic`, `cyrillic-ext`, `greek`, `greek-ext`, `vietnamese`), and emits one woff2 per bucket whose overlap is at least `MIN_GLYPHS_PER_BUCKET` (5) codepoints. A new `fonts.subset.css` is written next to the existing `fonts.css` with one `@font-face` per family per bucket carrying the matching `unicode-range:` declaration — interchangeable with what Google Fonts itself serves for a multi-script family. The DOM scrape is skipped in split-mode by design (split-mode is about ranged lazy-loading, not page-content subsetting). Optional value restricts to named buckets: `--split-ranges=latin,latin-ext`.
|
|
112
|
+
```bash
|
|
113
|
+
fontfetch subset https://stripe.com --split-ranges
|
|
114
|
+
fontfetch subset https://stripe.com --split-ranges=latin,latin-ext,vietnamese
|
|
115
|
+
```
|
|
116
|
+
- New public exports on `@fontfetch/core`:
|
|
117
|
+
- `FONT_FORMATS`, `isFontFormat`, `resolveFormat`, `filterFacesByFormat`, `urlMatchesFormat` (from a new `formats.ts` module)
|
|
118
|
+
- `parseUnicodeRange`, `formatUnicodeRange`, `GOOGLE_FONTS_RANGES`, `MIN_GLYPHS_PER_BUCKET`, `expandBucket` (from a new `codepoints.ts` module)
|
|
119
|
+
- Types: `FontFormat`, `UnicodeRangeBucket`, `SplitFamilyReport`
|
|
120
|
+
- New optional `PullOptions.formats` and `SubsetOptions.{ whitelist, splitRanges, splitBuckets }`. New optional `SubsetReport.{ splits, splitCss }` populated when `--split-ranges` is on.
|
|
121
|
+
|
|
122
|
+
### Notes
|
|
123
|
+
- No new runtime dependencies. The split flow reuses the existing `subset-font` peer dep (harfbuzzjs WASM) and the `fontkit` runtime dep that already powers `inspect` and `--fallback`.
|
|
124
|
+
- Bundle size unchanged at ~2.2 MB.
|
|
125
|
+
- The `--formats` filter is applied after the static + headless dedupe pass and before the filename-claim phase, so dropped faces never reach the downloader. Preload-link URLs (`<link rel="preload" as="font">`) are filtered by extension at the same point.
|
|
126
|
+
- Split-mode honours `pullResult.faces` to recover the original `font-weight` / `font-style` for each emitted `@font-face`. In `skipPull` mode (no parsed faces available) the chained CSS defaults to `400/normal` per face — the binaries are still split correctly; only the CSS metadata is best-effort.
|
|
127
|
+
- Test surface grew from 101 → 132 vitest cases (new: `formats` with 15 cases, `codepoints` with 16 cases). All green.
|
|
128
|
+
|
|
9
129
|
## [1.2.1] — 2026-05-28
|
|
10
130
|
|
|
11
131
|
The "discovery + empty-state" point release. Four small additions targeting the most common confusing outcomes after v1.2 shipped: silent variable-font collapses, partial Next.js subset captures, single-page entry blind spots, and the bare-bones "0 declarations found" terminal output.
|
|
@@ -82,7 +202,7 @@ The "inspect + subset + fallback" release. Three flagship subcommands ship toget
|
|
|
82
202
|
- Per-file progress log prefixes the bucket: `✓ google/Inter-Regular.woff2` instead of `✓ Inter-Regular.woff2`.
|
|
83
203
|
|
|
84
204
|
### Notes
|
|
85
|
-
- v0.5 was originally scoped as a static `preview.html`. We've decided to skip that and roll it into a much larger v0.5 — a hosted Next.js webapp at `fontfetch.dev` with live progress, foundry-style previews, side-by-side compare, and font-pairing. See [docs/roadmap.
|
|
205
|
+
- v0.5 was originally scoped as a static `preview.html`. We've decided to skip that and roll it into a much larger v0.5 — a hosted Next.js webapp at `fontfetch.dev` with live progress, foundry-style previews, side-by-side compare, and font-pairing. See [docs/roadmap.html](docs/roadmap.html#v05) for the public plan.
|
|
86
206
|
|
|
87
207
|
## [0.4.0] — 2026-05-27
|
|
88
208
|
|
package/README.md
CHANGED
|
@@ -62,21 +62,41 @@ Run on demand:
|
|
|
62
62
|
npx fontfetch <url>
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Install globally:
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
npm install -g fontfetch
|
|
69
69
|
fontfetch <url>
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
Or pick the distribution channel that fits your workflow (v1.4):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Homebrew tap (once published — see extensions/homebrew/)
|
|
76
|
+
brew install niyamvora/fontfetch/fontfetch
|
|
77
|
+
|
|
78
|
+
# GitHub Action (PR comments on font drift, CI release-gate)
|
|
79
|
+
# uses: niyamvora/fontfetch-action@v1
|
|
80
|
+
# See extensions/github-action/README.md
|
|
81
|
+
|
|
82
|
+
# Raycast extension (Cmd-Space → Extract Fonts from URL)
|
|
83
|
+
# See extensions/raycast/README.md
|
|
84
|
+
|
|
85
|
+
# Programmatic access to the pairings registry
|
|
86
|
+
npm install @fontfetch/registry
|
|
87
|
+
```
|
|
88
|
+
|
|
72
89
|
Requires Node 18+.
|
|
73
90
|
|
|
74
91
|
## Usage
|
|
75
92
|
|
|
76
93
|
```bash
|
|
77
|
-
fontfetch <url> [outDir] [--headless] [--pages <N>] [--fallback] [--emit ...] [--force]
|
|
94
|
+
fontfetch <url> [outDir] [--headless] [--pages <N>] [--fallback] [--emit ...] [--formats ...] [--force]
|
|
78
95
|
fontfetch inspect <font-file>
|
|
79
|
-
fontfetch subset <url> [outDir]
|
|
96
|
+
fontfetch subset <url> [outDir] [--whitelist <spec>] [--split-ranges[=<buckets>]]
|
|
97
|
+
fontfetch diff <urlA> <urlB> [outDir] [--json] # v1.4
|
|
98
|
+
fontfetch audit <url> [--max-kb N] [--per-family-kb F:N,...] [--no-commercial] [--json] # v1.4
|
|
99
|
+
fontfetch budget <url> --max-kb N [outDir] [--json] # v1.4
|
|
80
100
|
```
|
|
81
101
|
|
|
82
102
|
| Arg / Flag | Default | Notes |
|
|
@@ -85,9 +105,13 @@ fontfetch subset <url> [outDir]
|
|
|
85
105
|
| `[outDir]` | `./downloaded-fonts` | Per-site subfolder is created inside this |
|
|
86
106
|
| `--headless` | off | Launch Playwright/Chromium to also catch JS-loaded fonts |
|
|
87
107
|
| `--pages <N>` | `1` | Crawl up to N pages (entry + N-1 same-origin internal links) and merge fonts across all of them (v1.2.1). Max 50 |
|
|
88
|
-
| `--
|
|
89
|
-
| `--
|
|
108
|
+
| `--formats <list>` | — | Comma-separated allowlist of font formats to keep: `woff2`, `woff`, `ttf`, `otf`, `eot`. Faces with no matching source are dropped (v1.3). Default: keep every format the upstream CSS provides |
|
|
109
|
+
| `--fallback` | off | Emit a CLS-killing `<Family> Fallback` `@font-face` per family, with `size-adjust` / `ascent-override` / `descent-override` / `line-gap-override` matched via capsize metrics (v1.2). v1.3.1: monospace detection now reads the binary's `post.isFixedPitch` flag, not just the family name. v1.4: emits one block per (family, weight, style) tuple |
|
|
110
|
+
| `--gdpr-report` | off | Emit `GDPR.md` + `gdpr.json` listing every third-party font request with self-host remediation (v1.4) |
|
|
111
|
+
| `--emit <list>` | — | Framework configs: `next`, `tailwind`, `vite`, `tokens` (v1.4), `css` (default) |
|
|
90
112
|
| `--force` | off | Bypass the fail-fast check that blocks all-commercial sites |
|
|
113
|
+
| `--whitelist <spec>` (subset) | — | Extra codepoints to always include, on top of the DOM walk. CSS `unicode-range` syntax: `U+00A0,U+20AC,U+0020-007F` (v1.3) |
|
|
114
|
+
| `--split-ranges[=<buckets>]` (subset) | off | Emit one woff2 per Google Fonts language bucket (`latin`, `latin-ext`, `cyrillic`, `cyrillic-ext`, `greek`, `greek-ext`, `vietnamese`) and a chained `fonts.subset.css` (v1.3) |
|
|
91
115
|
|
|
92
116
|
Examples:
|
|
93
117
|
|
|
@@ -97,11 +121,77 @@ fontfetch https://linear.app ./public/fonts
|
|
|
97
121
|
fontfetch https://vercel.com /tmp/scratch
|
|
98
122
|
fontfetch https://some-spa.com --headless
|
|
99
123
|
fontfetch https://acme.com --pages=5
|
|
124
|
+
fontfetch https://shinobidata.com --formats=woff2
|
|
100
125
|
fontfetch https://stripe.com --headless --fallback --emit next
|
|
101
126
|
fontfetch inspect ./downloaded-fonts/example.com/files/google/Inter-Variable.woff2
|
|
102
127
|
fontfetch subset https://stripe.com
|
|
128
|
+
fontfetch subset https://stripe.com --whitelist=U+00A0,U+20AC
|
|
129
|
+
fontfetch subset https://stripe.com --split-ranges
|
|
103
130
|
```
|
|
104
131
|
|
|
132
|
+
### What's new in v1.4
|
|
133
|
+
|
|
134
|
+
**Eight features in one minor.** Four close out the engine work (competitor-gap closeouts from the [2026-05-28 research](./docs/research-competitor-feature-gaps-2026-05-28.md)) and four ship as distribution channels so fontfetch shows up where users already work.
|
|
135
|
+
|
|
136
|
+
#### Distribution channels
|
|
137
|
+
|
|
138
|
+
- **`@fontfetch/registry`** — new typed npm package. Consumes the community pairings registry with full autocomplete:
|
|
139
|
+
```bash
|
|
140
|
+
npm install @fontfetch/registry
|
|
141
|
+
```
|
|
142
|
+
```ts
|
|
143
|
+
import { findByFamily, freeAlternativesFor } from '@fontfetch/registry';
|
|
144
|
+
freeAlternativesFor('Söhne'); // ['Inter', 'Manrope', 'Outfit']
|
|
145
|
+
```
|
|
146
|
+
- **`fontfetch-action` GitHub Action** ([`extensions/github-action/`](./extensions/github-action)). PR comments on font drift; non-zero exit when budgets bust or commercial faces sneak in.
|
|
147
|
+
- **Raycast extension** ([`extensions/raycast/`](./extensions/raycast)). Three commands: extract fonts from a URL (CSS to clipboard), audit a URL (HUD verdict), search the pairings registry.
|
|
148
|
+
- **Homebrew Formula** ([`extensions/homebrew/`](./extensions/homebrew)). Source-of-truth tap Formula ready to publish to `homebrew-fontfetch` when warranted.
|
|
149
|
+
- **`--gdpr-report` flag.** Emits `GDPR.md` + `gdpr.json` listing every third-party font request with self-host remediation. Post-LG München I 20 O 1393/21 (2022) German court ruling on Google Fonts CDN.
|
|
150
|
+
- **Variable-font collapse hint.** When a family ships both a variable binary and ≥ 2 static weight files, fontfetch surfaces a one-liner with the byte saving.
|
|
151
|
+
|
|
152
|
+
#### Engine — release-gate capabilities
|
|
153
|
+
|
|
154
|
+
- **`fontfetch diff <urlA> <urlB>` — staging-vs-prod font drift.** Runs `pull()` on both URLs, prints added / removed / shared families with byte and commercial delta. `--json` for CI:
|
|
155
|
+
```bash
|
|
156
|
+
fontfetch diff https://staging.acme.com https://acme.com
|
|
157
|
+
fontfetch diff https://staging.acme.com https://acme.com --json
|
|
158
|
+
```
|
|
159
|
+
- **`fontfetch audit <url>` — CI release gate.** Non-zero exit on configured rule violations. Combine `--max-kb`, `--per-family-kb`, `--no-commercial`. Pairs with `--json` for downstream tools:
|
|
160
|
+
```bash
|
|
161
|
+
fontfetch audit https://acme.com --max-kb 200 --no-commercial
|
|
162
|
+
fontfetch audit https://acme.com --per-family-kb Inter:50,Geist:30 --json
|
|
163
|
+
```
|
|
164
|
+
- **`fontfetch budget <url> --max-kb N` — bundle-size budget shortcut.** Same engine as `audit` with only the size dimension wired. Drop-in for size-limit / Lighthouse-CI workflows.
|
|
165
|
+
- **`--emit tokens` — W3C / DTCG design tokens.** New emitter alongside `next` / `tailwind` / `vite`. Writes `fonts.tokens.json` with W3C Design Tokens Community Group ([tr.designtokens.org/format/](https://tr.designtokens.org/format/)) entries for every family + weight, plus a Tailwind-aligned size + line-height ladder. Drop into Style Dictionary, Tokens Studio for Figma, or Specify:
|
|
166
|
+
```bash
|
|
167
|
+
fontfetch https://vercel.com --emit tokens
|
|
168
|
+
```
|
|
169
|
+
- **`CONSISTENCY.md` cross-page report.** When `--pages > 1`, fontfetch writes a per-pull report of shared-vs-divergent families across crawled pages. *"Homepage uses Inter; `/blog` uses Tiempos"* — the report names the divergence per page. No competitor does this.
|
|
170
|
+
- **Per-weight Capsize fallback metrics.** `--fallback` now emits one `<Family> Fallback` block per (family, weight, style) tuple, each with matching `font-weight` and `font-style` declarations. Beats `fontaine` on their core feature ([fontaine #53](https://github.com/unjs/fontaine/issues/53), open 3+ years).
|
|
171
|
+
- **`provenance.json` machine-readable license + provenance.** Stable v1.0 schema. Shipped per pull alongside `LICENSE_REVIEW.md`. Consumed by `fontfetch audit`, the upcoming `fontfetch-action` GitHub Action, and external CI tools.
|
|
172
|
+
|
|
173
|
+
No new runtime dependencies; bundle size unchanged at ~2.2 MB.
|
|
174
|
+
|
|
175
|
+
### What's new in v1.3
|
|
176
|
+
|
|
177
|
+
Three additions that round out the subsetting pipeline. After v1.3, fontfetch takes a URL → folder, splits per Google Fonts language bucket, and runs entirely on Node — no Python required:
|
|
178
|
+
|
|
179
|
+
- **`--formats=woff2` modern-only emit.** Restricts the kept faces and downloaded files to a chosen format allowlist (one or more of `woff2`, `woff`, `ttf`, `otf`, `eot`). Addresses a long-standing community ask for modern-format-only output. Halves the typical bundle size on a modern-browser-only site:
|
|
180
|
+
```bash
|
|
181
|
+
fontfetch https://shinobidata.com --formats=woff2
|
|
182
|
+
```
|
|
183
|
+
- **`fontfetch subset --whitelist=U+00A0,U+20AC` — extra codepoints to always keep.** Same syntax as a CSS `unicode-range`. Pairs with the existing DOM-scrape pipeline so glyphs not rendered on page load (currency variants, breaking-space, icon-font glyphs injected by JS) stay alive in the subset. The `0x` shorthand is also accepted:
|
|
184
|
+
```bash
|
|
185
|
+
fontfetch subset https://stripe.com --whitelist=U+00A0,U+20AC,U+0020-007F
|
|
186
|
+
```
|
|
187
|
+
- **`fontfetch subset --split-ranges` — Google-Fonts-style per-language emit.** For every downloaded font, fontfetch intersects its character set against the canonical Google Fonts buckets (`latin`, `latin-ext`, `cyrillic`, `cyrillic-ext`, `greek`, `greek-ext`, `vietnamese`) and emits one woff2 per bucket plus a chained `fonts.subset.css` with `unicode-range:` declarations. The output is interchangeable with Google Fonts' own `css2` payload for a multi-script family. Browsers lazy-load only the buckets they need at runtime:
|
|
188
|
+
```bash
|
|
189
|
+
fontfetch subset https://stripe.com --split-ranges
|
|
190
|
+
fontfetch subset https://stripe.com --split-ranges=latin,latin-ext,vietnamese
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
No new runtime dependencies. The split flow reuses the existing `fontkit` runtime dep (already used by `inspect` and `--fallback`) and the `subset-font` peer dep.
|
|
194
|
+
|
|
105
195
|
### What's new in v1.2.1 — discovery + empty-state quick wins
|
|
106
196
|
|
|
107
197
|
Four small additions targeting the most common confusing outcomes of the v1.2 release:
|
|
@@ -214,29 +304,35 @@ No browser launched, no dependencies pulled at install time outside of TypeScrip
|
|
|
214
304
|
|
|
215
305
|
## How it compares
|
|
216
306
|
|
|
217
|
-
| Tool | Any URL | JS-rendered fonts | License classify | Framework emit | Inspect | Subset | Zero-CLS fallback |
|
|
218
|
-
|
|
219
|
-
| `google-webfonts-helper` | Google only | n/a | n/a | ✗ | ✗ | ✗ | ✗ |
|
|
220
|
-
| `webfont-dl` | needs CSS URL | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
|
221
|
-
| `glyphhanger` | ✓ (Puppeteer) | ✓ | ✗ | ✗ | ✗ | ✓ (Python `fonttools`) | ✗ |
|
|
222
|
-
| `fontaine` | n/a | n/a | n/a | partial | ✗ | ✗ | ✓ (Nuxt/Vite only) |
|
|
223
|
-
| `fontkit` | library, not a CLI | n/a | partial | ✗ | partial (library) | ✗ | ✗ |
|
|
224
|
-
| Chrome extensions | ✓ (manual) | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
|
225
|
-
| **`fontfetch`** | ✓ | ✓ | ✓ | ✓ next/tailwind/vite | ✓ | ✓ (Node, no Python) | ✓ framework-agnostic |
|
|
307
|
+
| Tool | Any URL | JS-rendered fonts | License classify | Framework emit | Inspect | Subset | Per-language split | Modern-only | Zero-CLS fallback | CI release-gate | Cross-page |
|
|
308
|
+
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
309
|
+
| `google-webfonts-helper` | Google only | n/a | n/a | ✗ | ✗ | ✗ | ✓ (Google catalog only) | ✓ | ✗ | ✗ | ✗ |
|
|
310
|
+
| `webfont-dl` | needs CSS URL | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
|
311
|
+
| `glyphhanger` | ✓ (Puppeteer) | ✓ | ✗ | ✗ | ✗ | ✓ (Python `fonttools`) | partial (unicode-range computed) | partial | ✗ | ✗ | ✗ |
|
|
312
|
+
| `fontaine` | n/a | n/a | n/a | partial | ✗ | ✗ | ✗ | n/a | ✓ family-wide (Nuxt/Vite only) | ✗ | ✗ |
|
|
313
|
+
| `fontkit` | library, not a CLI | n/a | partial | ✗ | partial (library) | ✗ | ✗ | n/a | ✗ | ✗ | ✗ |
|
|
314
|
+
| Chrome extensions | ✓ (manual) | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
|
315
|
+
| **`fontfetch`** | ✓ | ✓ | ✓ | ✓ next/tailwind/vite/**tokens** (v1.4) | ✓ | ✓ (Node, no Python) | ✓ Google Fonts buckets (v1.3) | ✓ `--formats=woff2` (v1.3) | ✓ **per-weight**, framework-agnostic (v1.4) | ✓ `audit` / `budget` / `diff` / `--json` (v1.4) | ✓ `CONSISTENCY.md` via `--pages` (v1.4) |
|
|
316
|
+
|
|
317
|
+
*"CI release-gate"* means non-zero exit codes on rule violations + `--json` output for downstream tooling. *"Cross-page"* means crawling multiple pages from a single entry URL and surfacing typography drift between them. Both are categories with zero competitors today.
|
|
226
318
|
|
|
227
319
|
## Roadmap
|
|
228
320
|
|
|
229
321
|
- [x] **v0.1** — Static `@font-face` extraction, ready-to-use CSS, manifest, README
|
|
230
|
-
- [x] **v0.1.1** — [Community font-pairing registry](./docs/roadmap.
|
|
322
|
+
- [x] **v0.1.1** — [Community font-pairing registry](./docs/roadmap.html#v011): share what fonts your favorite sites use, with free OFL alternatives
|
|
231
323
|
- [x] **v0.2** — `--headless` flag: Playwright mode for JS-loaded fonts (Adobe Typekit, SPAs, Cloudflare-protected sites)
|
|
232
324
|
- [x] **v0.2.2** — Referer-aware font downloads (unblocks foundry CDNs that 403 without a Referer)
|
|
233
325
|
- [x] **v0.3** — Framework emitters: `--emit next` / `tailwind` / `vite`
|
|
234
326
|
- [x] **v0.4** — License heuristic + `LICENSE_REVIEW.md` + fail-fast on all-commercial sites (`--force` to bypass)
|
|
235
327
|
- [x] **v0.6** — Provenance grouping: output split into `google/` / `adobe-typekit/` / `commercial/` / `open-cdn/` / `self-hosted/`
|
|
236
|
-
- [x] **v1.0** — [pnpm-workspaces monorepo restructure](./docs/roadmap.
|
|
237
|
-
- [x] **v1.2** — [Inspect + subset + fallback release](./docs/roadmap.
|
|
238
|
-
- [x] **v1.2.1** — [Discovery + empty-state quick wins](./docs/roadmap.
|
|
239
|
-
- [
|
|
328
|
+
- [x] **v1.0** — [pnpm-workspaces monorepo restructure](./docs/roadmap.html#v10): `@fontfetch/core` + the CLI, with `apps/` slots reserved for the webapp and headless worker
|
|
329
|
+
- [x] **v1.2** — [Inspect + subset + fallback release](./docs/roadmap.html#v12): `fontfetch inspect` (terminal Wakamai Fondue), `--fallback` (zero-CLS `@font-face` blocks via capsize), `fontfetch subset` (Playwright DOM scrape + harfbuzzjs subset, no Python). Plus `font-display: swap` default and preload-hint header on every emitted `fonts.css`.
|
|
330
|
+
- [x] **v1.2.1** — [Discovery + empty-state quick wins](./docs/roadmap.html#v121): variable-font hint after pull, Next.js subset sibling probe, `--pages <N>` multi-page crawl, focused 0-declaration output.
|
|
331
|
+
- [x] **v1.3** — [Modern emit + whitelist + per-language split](./docs/roadmap.html#v13): `--formats=woff2` modern-only emit, `subset --whitelist=U+00A0,…` extra codepoints, `subset --split-ranges` Google-Fonts-style per-language woff2 + chained `fonts.subset.css` with `unicode-range:` declarations.
|
|
332
|
+
- [x] **v1.3.1** — [Signal quality](./docs/roadmap.html#v131): `--fallback` reads `post.isFixedPitch` (catches Operator / PragmataPro / Berkeley Mono); license classifier cross-references the binary's `name` table (ids 13 + 14); `LICENSE_REVIEW.md` calls out OFL Reserved Font Name families.
|
|
333
|
+
- [x] **v1.4** — [CI release-gate + distribution channels](./docs/roadmap.html#v14): engine = `fontfetch diff` / `audit` / `budget` + `--emit tokens` + `--gdpr-report` + per-weight Capsize fallback + cross-page `CONSISTENCY.md` + machine-readable `provenance.json` + variable-font collapse hint. Channels = [`@fontfetch/registry`](./packages/registry) typed npm package + [`fontfetch-action`](./extensions/github-action) GitHub Action + [Raycast extension](./extensions/raycast) + [Homebrew tap](./extensions/homebrew).
|
|
334
|
+
- [ ] **v1.5** — [Prototype-grade font morphing](./docs/roadmap.html#v15): `fontfetch morph <file> --round --width --slant --weight --rename`. Pre-commission sketchbook for designers — four sliders, real binary out, OFL-rename-enforced. Webapp `/edit/[id]` with live preview + share-to-client links lands in v1.6; community preset library in v1.7.
|
|
335
|
+
- [ ] **v0.5** — [Hosted webapp at `fontfetch.dev`](./docs/roadmap.html#v05): URL → live progress → foundry-style previews → compare + pairing
|
|
240
336
|
|
|
241
337
|
Want one of these sooner? Open an issue or vote on existing ones.
|
|
242
338
|
|