9.5 KiB
Font compress CLI
Package: @suaveplan/font-compress
Status: PROPOSED
Type: spec-conformance + feature
Why
The 2026-05-02 audit of @suaveplan/font-compress against legacy packages/universal/font-compress/ found that genesis ships only the library surface (BinaryFontPacker, BinaryFontUnpacker). Legacy ships a real font-compress bin (a 369-line cli.ts, registered as bin: { "font-compress": "./dist/cli.js" } in packages/universal/font-compress/package.json) that takes a TTF/OTF/WOFF/WOFF2 path or directory, runs it through batchConvertFonts from the legacy in-package converter, packs the output as .btf files, and writes a font registry JSON. Every existing consumer of the legacy package (font preprocessing pipelines, build steps, R3F asset baking) calls the bin, not the library API. Without it the package is unusable as a build-time tool.
Genesis correctly split the converter step out into @suaveplan/font-convert (FontConverter, BatchConverter), so the new CLI composes the two: convert → pack → write. The library surface stays unchanged; this change adds a thin bin layer that wires the two packages together with a conventional CLI argument parser, progress streaming, and structured exit codes.
What changes
- ADDED:
binentry inpackage.json—"suaveplan-font-compress": "./dist/bin/suaveplan-font-compress.js". Bin name uses the genesissuaveplan-*convention (consistent withsuaveplan-install). - ADDED:
src/bin/suaveplan-font-compress.ts— the CLI entry point. Built by Vite intodist/bin/suaveplan-font-compress.jswith a#!/usr/bin/env nodeshebang preserved. - ADDED:
src/bin/suaveplan-font-compress.md— sibling doc covering invocation patterns, every flag, exit codes, and example pipelines. - ADDED: argument parser supporting:
- positional
<input>— file (.ttf/.otf/.woff/.woff2/.json) or directory path --input <path>/-i <path>— explicit input (alternative to positional)--output <path>/-o <path>— output directory--format=btf|json— output format; defaultbtf--force/-f— overwrite existing.btf/.jsonfiles--recursive/-r— recurse directories (defaulttrue)--no-recursive— disable recursion--preserve-structure/--no-preserve-structure— directory-layout strategy--help/-h— print usage--version/-v— print version (read frompackage.json)
- positional
- ADDED: input dispatcher — detects whether
<input>is typeface JSON (treat as already-converted, pack only) or a font binary (convert first via@suaveplan/font-convert, then pack). Single-file and directory inputs both supported; directory inputs useBatchConverterfrom@suaveplan/font-convert. - ADDED:
--format=jsonmode — emit converted typeface JSON instead of BTF binary (useful for debugging the pipeline). - ADDED: structured exit codes:
0— all inputs processed successfully1— one or more inputs failed during conversion or packing2— invalid CLI arguments (unknown flag, missing required positional, conflicting flags)3— input path does not exist
- ADDED: error code
FONT_COMPRESS.CLI_INVALID_ARGSregistered on the package's existingFONT_COMPRESS_CODESregistry (same pattern asBarcodeError); thrown on malformed CLI input before any FS work. - ADDED: dev dependency on
@suaveplan/cli(for argument parsing, ANSI styling, progress bars) and@suaveplan/readline(for interactive prompts on overwrite confirmation). - MODIFIED:
package.jsonbinfield added;dependenciesgain@suaveplan/cli,@suaveplan/readline,@suaveplan/logger;dependenciesretain@suaveplan/font-convert(already present).
Out of scope
- Changing the library surface (
BinaryFontPacker,BinaryFontUnpacker) — they stay byte-for-byte identical. The CLI is purely a composition layer above them. - Re-merging
font-convertback intofont-compress— the genesis split is intentional (CLAUDE.md explicitly notes "the conversion step itself was intentionally split to@suaveplan/font-convert"). The CLI imports the converter; it does not re-host it. - Auto-installing as a
binonbun add @suaveplan/font-compress— that's a registry / lifecycle concern, not a spec concern. Thebinfield inpackage.jsonis enough; npm/bun do the rest. - WASM-backed conversion or hardware-accelerated packing — this CLI uses the existing TypeScript pipeline. A WASM optimisation is its own change.
- Watch mode (
--watchre-running on file changes) — punted; legacy doesn't have it. If demanded, add it later under a separate change.
Source mapping
This change derives from ../suaveplan/packages/universal/font-compress/src/cli.ts.
| Requirement | Source lines | Notes |
|---|---|---|
bin field shape ("font-compress": "./dist/cli.js") |
package.json L8-L10 |
Renamed: legacy bin is font-compress; new bin is suaveplan-font-compress (genesis convention) |
showHelp printed usage |
cli.ts L33-L88 |
Reworded for genesis flag set; layout preserved |
showVersion reading from package.json |
cli.ts L93-L99 |
Verbatim semantics; switch to ESM import of package.json |
Argument parser (-h, -v, -f, -r, --no-recursive, --preserve-structure, --no-preserve-structure, -o, --output) |
cli.ts L104-L180 |
Verbatim flag set + adds --format=btf|json,-i/--input |
Input-validation flow (existence, defaults based on outputSpecified) |
cli.ts L185-L240 |
Verbatim semantics |
Progress callback (onProgress consuming ConversionProgress) |
cli.ts L253-L320 |
Verbatim; routed through @suaveplan/cli progress bar instead of inline ANSI codes |
| Summary block (totals, compression ratio, output paths) | cli.ts L322-L353 |
Verbatim formatting |
| Exit codes 0 / 1 (success / errors) | cli.ts L356-L362, L367-L368 |
Verbatim |
Requirements with no source line
| Requirement | Justification |
|---|---|
| Exit codes 2 (bad args) and 3 (missing input) | Legacy collapses both into exit 1. Genesis-wide guardrail — distinct exit codes let CI scripts tell "user typed it wrong" from "the work failed". Industry-standard convention (curl, ffmpeg, etc.). |
--format=json mode |
Legacy is BTF-only. Adding the JSON path makes the CLI debuggable — operators can inspect the typeface intermediate before the binary pack — and it's a one-line addition since @suaveplan/font-convert already produces TypefaceFont JSON. |
FONT_COMPRESS.CLI_INVALID_ARGS error code |
CLAUDE.md §Workflow rule 2 — every thrown error routes through @suaveplan/error with a stable code. Legacy uses process.exit(1) after console.error, which loses the error code. |
Bin name suaveplan-font-compress (not font-compress) |
Genesis convention — suaveplan-install (@suaveplan/installer) is the existing precedent. Avoids global-namespace collisions with arbitrary font-compress bins from other authors. |
Composition with @suaveplan/font-convert for binary input |
Genesis split the converter into its own package; the legacy CLI's in-package converter no longer exists in genesis. Importing @suaveplan/font-convert is the only way to handle TTF/OTF/WOFF/WOFF2 input. |
Verification approach
src/bin/suaveplan-font-compress.test.ts— covers argument parsing (every flag, conflicting flags, missing positional, unknown flag), exit-code mapping,--help/--versionshort-circuit, dispatch between JSON-input and binary-input paths.src/bin/suaveplan-font-compress.spec-conformance.test.ts— pinsFONT_COMPRESS.CLI_INVALID_ARGSemission, structured logging through@suaveplan/logger, OTel spanfont-compress.cli.runrecorded against an in-memory tracer, exit code never being thrown as a number from a non-mainsite.src/bin/integration/cli-roundtrip.test.ts— integration test using a fixture TTF: invokes the bin viaBun.spawn, verifies stdout contains the success banner, verifies a.btffile is written, verifies the.btfround-trips throughBinaryFontUnpackerto aTypefaceFontdeep-equal to the converter output.
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/font-compress to enumerate. As of 2026-05-02 there are no in-repo dependents:
- No dependents (verified by
grep -rl "@suaveplan/font-compress" /home/ubuntu/web/genesis/packages/ --include="package.json"returning onlypackages/fonts/font-compress/package.jsonitself).
This change is purely additive (new bin, new error code, new module under src/bin/). The library exports are unchanged, so the absence of dependents is not a risk gate.
Migration impact
None for library callers — BinaryFontPacker and BinaryFontUnpacker are unchanged. New for CLI callers — once the package is installed (bun add -D @suaveplan/font-compress) the suaveplan-font-compress bin is on node_modules/.bin/. Legacy callers using the old font-compress bin name will need to update their build scripts; this is captured in the ## Migration block of this proposal:
Migration
- Old behavior:
npx font-compress ./fonts -o ./output - New behavior:
bunx suaveplan-font-compress ./fonts -o ./output(or add"compress:fonts": "suaveplan-font-compress ./fonts"topackage.jsonscripts) - Search command consumers should run:
grep -rln "\\bfont-compress\\b" .in their build scripts and CI workflows - Recommended replacement: rename invocations to
suaveplan-font-compress. The flag set is fully back-compatible (every legacy flag still works); only the bin name changed, plus the new optional--format=jsonis added.