15 KiB
Capability Spec: font-compress
Package: @suaveplan/font-compress
Status: SHIPPED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions.
Purpose
@suaveplan/font-compress packs TypefaceFont JSON objects from @suaveplan/font-convert into the BTF (Binary TypeFace) compact wire format. It is the build-time side of the BTF pipeline; the sibling @suaveplan/font-decompress reverses the operation at runtime. Post-split the package's scope is packer-only — every unpacker, decoder, and unpacker-side helper now lives in @suaveplan/font-decompress, so renderer-tier consumers (which never pack) can install only what they execute.
Requirements
Requirement: Packer-only public surface
The package SHALL expose BinaryFontPacker class + createBinaryFontPacker() factory + FONT_COMPRESS_CODES registry + FontCompressError class, and SHALL NOT expose any unpacker functionality. BinaryFontUnpacker and createBinaryFontUnpacker are NOT exported from this package — they live in the sibling @suaveplan/font-decompress.
Scenario: root barrel exposes only the packer
- WHEN a consumer imports
@suaveplan/font-compress - THEN
BinaryFontPacker,createBinaryFontPacker,FONT_COMPRESS_CODES, andFontCompressErrorare present;BinaryFontUnpackeris undefined
Verified by: src/packer/binary-font-packer/binary-font-packer.test.ts::"root barrel exposes packer only"
Requirement: FONT_COMPRESS_CODES trimmed registry
The package SHALL declare an error-code registry via defineCodes("FONT_COMPRESS", [...]) containing exactly PACK_FAILED and INVALID_DATA. UNPACK_FAILED is NOT in this registry — it lives in @suaveplan/font-decompress as FONT_DECOMPRESS.UNPACK_FAILED. Every pack-side error path SHALL throw a FontCompressError carrying one of the two retained codes.
Scenario: every kept code is reachable
- WHEN the test suite exercises a successful pack and a malformed-input pack (e.g. missing
familyName) - THEN the malformed path raises
FontCompressErrorwhosecodematchesFONT_COMPRESS.INVALID_DATAorFONT_COMPRESS.PACK_FAILED
Scenario: UNPACK_FAILED removed from registry
- WHEN the test suite asserts on
Object.keys(FONT_COMPRESS_CODES).sort() - THEN the array is exactly
["INVALID_DATA", "PACK_FAILED"](sorted lexicographically)
Verified by: src/error-codes.test.ts::"every kept FONT_COMPRESS.* code is reachable" AND src/error-codes.test.ts::"UNPACK_FAILED removed from registry post-split"
Requirement: BinaryFontPacker public API
The package SHALL provide class BinaryFontPacker with method pack(font: TypefaceFont): Uint8Array and a createBinaryFontPacker(): BinaryFontPacker factory function. TypefaceFont is imported from @suaveplan/font-convert. The class SHALL emit the BTF wire format consumed by @suaveplan/font-decompress::BinaryFontUnpacker.
Scenario: round-trip TypefaceFont → BTF → TypefaceFont
- WHEN a known-vector
TypefaceFontis passed toBinaryFontPacker.pack(...)and the resulting bytes are then unpacked viaBinaryFontUnpacker.unpack(...)(from@suaveplan/font-decompress) - THEN the round-trip preserves
familyName,cssFontWeight,cssFontStyle,boundingBox, metrics, and every glyph entry (within the dictionary-encoding tolerance documented in the implementation)
Verified by: src/packer/binary-font-packer/binary-font-packer.test.ts::"round-trip preserves TypefaceFont shape via font-decompress unpacker"
Requirement: encodeString utility
The package SHALL provide an encodeString(value: string): Uint8Array helper used by the packer to emit length-prefixed UTF-8 strings to the BTF header. The function SHALL emit the bytes in the format [uint32 length][utf8 bytes]. decodeString is NOT in this package — it lives in @suaveplan/font-decompress.
Scenario: emits length-prefixed UTF-8
- WHEN
encodeString("Inter Variable")is called - THEN the returned
Uint8Arrayis14(uint32 length, little-endian) followed by the UTF-8 bytes of"Inter Variable"
Scenario: decodeString export removed
- WHEN
import { decodeString } from "@suaveplan/font-compress"is attempted at type-check time - THEN TypeScript reports
decodeStringis not exported (the test asserts the runtime absence viaimport * as ns from ...; expect(ns).not.toHaveProperty("decodeString"))
Verified by: src/utils/encode-string.test.ts::"emits length-prefixed UTF-8" AND src/utils/encode-string.test.ts::"decodeString export removed post-split"
Requirement: Universal-tier purity
The package SHALL ship as a universal-tier package — no imports from node:*, bun:*, or the bare specifiers fs/path/stream/net/http/crypto in any file under src/. package.json SHALL NOT declare a "browser" field, SHALL NOT pin engines.node or engines.bun, and SHALL declare "sideEffects": false.
Scenario: no platform imports in src/
- WHEN the CI step
grep -rnE "from ['\"](node:|bun:|fs|path|stream|net|http)['\"]" packages/fonts/font-compress/src/runs - THEN the command produces zero matches
Verified by: src/packer/binary-font-packer/binary-font-packer.test.ts::"tier purity: no node/bun imports"
Requirement: Subpath exports trimmed
package.json SHALL declare subpath exports for . (root barrel), ./error-codes, ./constants, ./utils/encode-string, ./utils/parse-path, ./packer/binary-font-packer. The ./unpacker/binary-font-unpacker subpath is removed (no longer present in dist).
Scenario: unpacker subpath removed
- WHEN a consumer attempts
import { BinaryFontUnpacker } from "@suaveplan/font-compress/unpacker/binary-font-unpacker" - THEN the import fails to resolve (the subpath is no longer in
package.jsonexports and the source files are no longer present)
Verified by: src/packer/binary-font-packer/binary-font-packer.test.ts::"unpacker subpath export removed"
Requirement: Banned patterns absent from src/
The package SHALL ship with zero matches of @deprecated, TODO, FIXME, ts-ignore, ts-expect-error, it.skip, test.skip, describe.skip, .only, raw any, or console.log in any .ts or .tsx file under src/. Zero // biome-ignore suppression comments. All test files import runner primitives from @suaveplan/testing/runner (rule §7).
Scenario: clean static-analysis grep
- WHEN the CI step
grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\\.skip\\|\\.only\\|\\bany\\b\\|console\\.log" packages/fonts/font-compress/src/ --include="*.ts"runs - THEN the command produces zero matches
Verified by: repo-wide CI grep gate + bun scripts/lint/check-test-runner-imports.ts
Requirement: suaveplan-font-compress bin entry
The package SHALL register a bin entry named suaveplan-font-compress mapping to ./dist/bin/suaveplan-font-compress.js. The compiled bin script SHALL start with #!/usr/bin/env node and SHALL be executable. Invoking it from a consumer's node_modules/.bin/ SHALL run the CLI's main function with process.argv.slice(2).
Scenario: bin field declared in package.json
- WHEN the package's
package.jsonis parsed - THEN
bin["suaveplan-font-compress"] === "./dist/bin/suaveplan-font-compress.js"
Scenario: shebang preserved in compiled output
- WHEN
dist/bin/suaveplan-font-compress.jsis read afterbunx turbo build --filter=@suaveplan/font-compress - THEN the first line is exactly
#!/usr/bin/env node
Verified by: src/bin/suaveplan-font-compress.test.ts::"package.json bin field maps suaveplan-font-compress to dist/bin entry" AND src/bin/suaveplan-font-compress.spec-conformance.test.ts::"compiled bin starts with node shebang"
Requirement: Argument parser with help and version
The package SHALL accept the following flags: positional <input>; --input <path> / -i <path>; --output <path> / -o <path>; --format=btf|json; --force / -f; --recursive / -r; --no-recursive; --preserve-structure; --no-preserve-structure; --help / -h; --version / -v. --help SHALL print usage to stdout and exit 0. --version SHALL print the package version (read from package.json) and exit 0. Unknown flags SHALL print an error to stderr and exit 2. Conflicting flags (e.g. --recursive and --no-recursive together) SHALL print an error to stderr and exit 2.
Scenario: --help short-circuits without doing FS work
- WHEN the bin is invoked with
["--help"] - THEN stdout contains the string
"USAGE"AND the exit code is0
Scenario: --version reads from package.json
- WHEN the bin is invoked with
["--version"] - THEN stdout contains the same version string as
JSON.parse(readFileSync('package.json')).versionAND the exit code is0
Scenario: unknown flag exits 2
- WHEN the bin is invoked with
["--no-such-flag"] - THEN stderr contains the string
"Unknown option"AND the exit code is2
Scenario: conflicting recursive flags exit 2
- WHEN the bin is invoked with
["./fonts", "--recursive", "--no-recursive"] - THEN the exit code is
2AND stderr describes the conflict
Verified by: src/bin/suaveplan-font-compress.test.ts::"--help short-circuits with exit 0" AND src/bin/suaveplan-font-compress.test.ts::"--version prints package version with exit 0" AND src/bin/suaveplan-font-compress.test.ts::"unknown flag exits 2" AND src/bin/suaveplan-font-compress.test.ts::"conflicting recursive flags exit 2"
Requirement: Input dispatcher selects convert + pack vs pack-only
The package SHALL inspect the input path's extension. For .ttf, .otf, .woff, .woff2 (and directories containing them when --recursive) the bin SHALL call @suaveplan/font-convert::FontConverter (single file) or BatchConverter (directory) to produce a TypefaceFont, then call BinaryFontPacker.pack(typefaceFont) to emit a Uint8Array, then write the .btf file. For .json inputs (a previously-converted typeface JSON) the bin SHALL skip conversion and pack directly after schema validation.
Scenario: TTF input runs convert then pack
- WHEN the bin is invoked with a single
.ttffile path - THEN
FontConverter.convertis called once ANDBinaryFontPacker.packis called once with the converter's output AND a.btffile is written
Scenario: JSON input runs pack only
- WHEN the bin is invoked with a single
.jsonfile path containing a validTypefaceFont - THEN
FontConverter.convertis not called ANDBinaryFontPacker.packis called once with the parsed JSON AND a.btffile is written
Scenario: directory input uses BatchConverter
- WHEN the bin is invoked with a directory path containing two
.ttffiles at--recursive - THEN
BatchConverteris constructed ANDBinaryFontPacker.packis called twice AND two.btffiles are written under--output
Verified by: src/bin/suaveplan-font-compress.test.ts::"TTF input runs convert then pack" AND src/bin/suaveplan-font-compress.test.ts::"JSON input runs pack only" AND src/bin/integration/cli-roundtrip.test.ts::"directory of TTFs produces matching BTF outputs"
Requirement: --format=json emits typeface JSON instead of BTF
The package SHALL accept --format=json to emit the converter's TypefaceFont as JSON instead of running it through BinaryFontPacker. This mode SHALL write <basename>.json files. JSON-input + --format=json SHALL be a no-op pass-through (read JSON, validate, write to output path).
Scenario: --format=json writes typeface JSON
- WHEN the bin is invoked with a
.ttfinput and--format=json - THEN a
.jsonfile is written ANDBinaryFontPacker.packis not called AND the JSON parses as a validTypefaceFont
Scenario: default format is btf
- WHEN the bin is invoked without
--format - THEN
--format=btfis the effective default AND.btffiles are written
Verified by: src/bin/suaveplan-font-compress.test.ts::"--format=json writes typeface JSON instead of BTF" AND src/bin/suaveplan-font-compress.test.ts::"default format is btf"
Requirement: Structured exit codes
The package SHALL return distinct exit codes: 0 all inputs succeeded; 1 one or more inputs failed during conversion or packing (per-file errors isolated; batch continues); 2 invalid CLI arguments (unknown flag, missing required positional, conflicting flags); 3 the input path does not exist on disk.
Scenario: missing input path exits 3
- WHEN the bin is invoked with a positional input that does not exist
- THEN the exit code is
3AND stderr contains the string"does not exist"
Scenario: per-file failure exits 1, not 2
- WHEN the bin is invoked over a directory where one TTF file is corrupt and three are valid
- THEN the exit code is
1AND three.btffiles are written AND the corrupt file's error is captured to stderr
Verified by: src/bin/suaveplan-font-compress.test.ts::"missing input path exits 3" AND src/bin/integration/cli-roundtrip.test.ts::"corrupt TTF in directory yields exit 1 with siblings still written"
Requirement: FONT_COMPRESS.CLI_INVALID_ARGS error code
The package SHALL register FONT_COMPRESS.CLI_INVALID_ARGS in FONT_COMPRESS_CODES. Argument-parsing failures SHALL throw FontCompressError with this code (caught by main's top-level handler, mapped to exit 2). Plain Error SHALL NOT be thrown from the CLI argument-parsing path.
Scenario: code is registered
- WHEN
FONT_COMPRESS_CODESis enumerated - THEN the array includes
"FONT_COMPRESS.CLI_INVALID_ARGS"
Scenario: bad args throw the coded error
- WHEN the bin is invoked with an unknown flag and the throw is intercepted (in-process driver, not subprocess)
- THEN a
FontCompressErroris thrown withcode === "FONT_COMPRESS.CLI_INVALID_ARGS"ANDcontext.flag === "--no-such-flag"
Verified by: src/error-codes.test.ts::"FONT_COMPRESS_CODES includes CLI_INVALID_ARGS" AND src/bin/suaveplan-font-compress.spec-conformance.test.ts::"argument-parser throws FontCompressError with CLI_INVALID_ARGS"
Requirement: Progress reporting and summary
The package SHALL print per-file progress lines while processing a batch (one line per converted file with the family name and compression ratio) and a summary block at the end (total files, succeeded, failed, total original bytes, total compressed bytes, compression ratio, output path). Progress lines SHALL go to stdout. Errors SHALL go to stderr.
Scenario: per-file progress lines printed
- WHEN the bin is invoked over a directory with three valid TTFs
- THEN stdout contains three lines starting with the per-file marker (
✓or equivalent) AND each line names the family
Scenario: summary block printed at end
- WHEN the same invocation completes
- THEN stdout ends with a summary block containing
Total fonts: 3,Converted: 3,Errors: 0AND a compression-ratio line
Verified by: src/bin/integration/cli-roundtrip.test.ts::"per-file progress lines and summary block printed"