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 +53 -0
- package/README.md +40 -12
- package/dist/cli.js +16271 -15810
- package/dist/cli.js.map +1 -1
- package/package.json +14 -11
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
|
-
| `--
|
|
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.
|