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 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.md](docs/roadmap.md#v05--hosted-webapp) for the public plan.
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
- Or install globally:
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
- | `--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) |
89
- | `--emit <list>` | | Framework configs: `next`, `tailwind`, `vite`, `css` (default) |
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.md#v011--community-font-pairing-registry): share what fonts your favorite sites use, with free OFL alternatives
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.md#v10--monorepo-restructure--shipped): `@fontfetch/core` + the CLI, with `apps/` slots reserved for the webapp and headless worker
237
- - [x] **v1.2** — [Inspect + subset + fallback release](./docs/roadmap.md#v12--flagship-inspect--subset--fallback-release--shipped-2026-05-28): `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`.
238
- - [x] **v1.2.1** — [Discovery + empty-state quick wins](./docs/roadmap.md#v121--discovery--empty-state-quick-wins--shipped): variable-font hint after pull, Next.js subset sibling probe, `--pages <N>` multi-page crawl, focused 0-declaration output.
239
- - [ ] **v0.5** — [Hosted webapp at `fontfetch.dev`](./docs/roadmap.md#v05--hosted-webapp): URL live progress foundry-style previews compare + pairing
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