fontfetch 1.2.1 → 1.3.1

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,59 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.1] — 2026-05-29
10
+
11
+ 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`.
12
+
13
+ ### Changed
14
+ - **`--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.
15
+ - **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.
16
+ - **`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.
17
+
18
+ ### Added
19
+ - New public export on `@fontfetch/core`: `crossRefLicenseFromBinaries(classified, filesDir) → ClassifiedFace[]` (from a new `license/binary-license.ts` module).
20
+ - `LicenseClassification` gains an optional `hasRFN?: boolean` field. Set by the cross-ref pass; consumed by `buildLicenseReview`.
21
+ - `InspectionReport` gains an `isFixedPitch: boolean` field. Read from `font.post?.isFixedPitch` (boolean or uint32 — both shapes are coerced).
22
+ - `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.
23
+
24
+ ### Notes
25
+ - No new runtime dependencies. The cross-ref pass reuses the existing `inspect()` helper; the fallback pass reuses the existing `fontkit` runtime dep.
26
+ - Bundle size unchanged at ~2.2 MB.
27
+ - The cross-ref pass is non-fatal: missing files, parse failures, and absent OpenType tables all degrade gracefully back to the URL-signature classification.
28
+ - 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.
29
+
30
+ ## [1.3.0] — 2026-05-28
31
+
32
+ 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.
33
+
34
+ ### Added
35
+ - **`--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.
36
+ ```bash
37
+ fontfetch https://shinobidata.com --formats=woff2 # modern-only output, halves bundle size
38
+ fontfetch https://acme.com --formats=woff2,woff # slight legacy reach
39
+ ```
40
+ - **`--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.
41
+ ```bash
42
+ fontfetch subset https://stripe.com --whitelist=U+00A0,U+20AC
43
+ ```
44
+ - **`--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`.
45
+ ```bash
46
+ fontfetch subset https://stripe.com --split-ranges
47
+ fontfetch subset https://stripe.com --split-ranges=latin,latin-ext,vietnamese
48
+ ```
49
+ - New public exports on `@fontfetch/core`:
50
+ - `FONT_FORMATS`, `isFontFormat`, `resolveFormat`, `filterFacesByFormat`, `urlMatchesFormat` (from a new `formats.ts` module)
51
+ - `parseUnicodeRange`, `formatUnicodeRange`, `GOOGLE_FONTS_RANGES`, `MIN_GLYPHS_PER_BUCKET`, `expandBucket` (from a new `codepoints.ts` module)
52
+ - Types: `FontFormat`, `UnicodeRangeBucket`, `SplitFamilyReport`
53
+ - New optional `PullOptions.formats` and `SubsetOptions.{ whitelist, splitRanges, splitBuckets }`. New optional `SubsetReport.{ splits, splitCss }` populated when `--split-ranges` is on.
54
+
55
+ ### Notes
56
+ - 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`.
57
+ - Bundle size unchanged at ~2.2 MB.
58
+ - 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.
59
+ - 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.
60
+ - Test surface grew from 101 → 132 vitest cases (new: `formats` with 15 cases, `codepoints` with 16 cases). All green.
61
+
9
62
  ## [1.2.1] — 2026-05-28
10
63
 
11
64
  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.
package/README.md CHANGED
@@ -74,9 +74,9 @@ Requires Node 18+.
74
74
  ## Usage
75
75
 
76
76
  ```bash
77
- fontfetch <url> [outDir] [--headless] [--pages <N>] [--fallback] [--emit ...] [--force]
77
+ fontfetch <url> [outDir] [--headless] [--pages <N>] [--fallback] [--emit ...] [--formats ...] [--force]
78
78
  fontfetch inspect <font-file>
79
- fontfetch subset <url> [outDir]
79
+ fontfetch subset <url> [outDir] [--whitelist <spec>] [--split-ranges[=<buckets>]]
80
80
  ```
81
81
 
82
82
  | Arg / Flag | Default | Notes |
@@ -85,9 +85,12 @@ fontfetch subset <url> [outDir]
85
85
  | `[outDir]` | `./downloaded-fonts` | Per-site subfolder is created inside this |
86
86
  | `--headless` | off | Launch Playwright/Chromium to also catch JS-loaded fonts |
87
87
  | `--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) |
88
+ | `--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 |
89
+ | `--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 |
89
90
  | `--emit <list>` | — | Framework configs: `next`, `tailwind`, `vite`, `css` (default) |
90
91
  | `--force` | off | Bypass the fail-fast check that blocks all-commercial sites |
92
+ | `--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) |
93
+ | `--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
94
 
92
95
  Examples:
93
96
 
@@ -97,11 +100,34 @@ fontfetch https://linear.app ./public/fonts
97
100
  fontfetch https://vercel.com /tmp/scratch
98
101
  fontfetch https://some-spa.com --headless
99
102
  fontfetch https://acme.com --pages=5
103
+ fontfetch https://shinobidata.com --formats=woff2
100
104
  fontfetch https://stripe.com --headless --fallback --emit next
101
105
  fontfetch inspect ./downloaded-fonts/example.com/files/google/Inter-Variable.woff2
102
106
  fontfetch subset https://stripe.com
107
+ fontfetch subset https://stripe.com --whitelist=U+00A0,U+20AC
108
+ fontfetch subset https://stripe.com --split-ranges
103
109
  ```
104
110
 
111
+ ### What's new in v1.3
112
+
113
+ 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:
114
+
115
+ - **`--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:
116
+ ```bash
117
+ fontfetch https://shinobidata.com --formats=woff2
118
+ ```
119
+ - **`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:
120
+ ```bash
121
+ fontfetch subset https://stripe.com --whitelist=U+00A0,U+20AC,U+0020-007F
122
+ ```
123
+ - **`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:
124
+ ```bash
125
+ fontfetch subset https://stripe.com --split-ranges
126
+ fontfetch subset https://stripe.com --split-ranges=latin,latin-ext,vietnamese
127
+ ```
128
+
129
+ 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.
130
+
105
131
  ### What's new in v1.2.1 — discovery + empty-state quick wins
106
132
 
107
133
  Four small additions targeting the most common confusing outcomes of the v1.2 release:
@@ -214,15 +240,15 @@ No browser launched, no dependencies pulled at install time outside of TypeScrip
214
240
 
215
241
  ## How it compares
216
242
 
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 |
243
+ | Tool | Any URL | JS-rendered fonts | License classify | Framework emit | Inspect | Subset | Per-language split | Modern-only | Zero-CLS fallback |
244
+ |---|---|---|---|---|---|---|---|---|---|
245
+ | `google-webfonts-helper` | Google only | n/a | n/a | ✗ | ✗ | ✗ | ✓ (Google catalog only) | ✓ | ✗ |
246
+ | `webfont-dl` | needs CSS URL | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
247
+ | `glyphhanger` | ✓ (Puppeteer) | ✓ | ✗ | ✗ | ✗ | ✓ (Python `fonttools`) | partial (unicode-range computed) | partial | ✗ |
248
+ | `fontaine` | n/a | n/a | n/a | partial | ✗ | ✗ | ✗ | n/a | ✓ (Nuxt/Vite only) |
249
+ | `fontkit` | library, not a CLI | n/a | partial | ✗ | partial (library) | ✗ | ✗ | n/a | ✗ |
250
+ | Chrome extensions | ✓ (manual) | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
251
+ | **`fontfetch`** | ✓ | ✓ | ✓ | ✓ next/tailwind/vite | ✓ | ✓ (Node, no Python) | ✓ Google Fonts buckets (v1.3) | ✓ `--formats=woff2` (v1.3) | ✓ framework-agnostic |
226
252
 
227
253
  ## Roadmap
228
254
 
@@ -236,6 +262,8 @@ No browser launched, no dependencies pulled at install time outside of TypeScrip
236
262
  - [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
263
  - [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
264
  - [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.
265
+ - [x] **v1.3** — [Modern emit + whitelist + per-language split](./docs/roadmap.md#v13--shipped-2026-05-28): `--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.
266
+ - [x] **v1.3.1** — [Signal quality](./docs/roadmap.md#v131--signal-quality--shipped-2026-05-29): `--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.
239
267
  - [ ] **v0.5** — [Hosted webapp at `fontfetch.dev`](./docs/roadmap.md#v05--hosted-webapp): URL → live progress → foundry-style previews → compare + pairing
240
268
 
241
269
  Want one of these sooner? Open an issue or vote on existing ones.