Files
2026-05-27 23:29:53 +01:00

9.6 KiB
Raw Permalink Blame History

Split @suaveplan/font-compress into compress + decompress packages

Packages: @suaveplan/font-compress (modified), @suaveplan/font-decompress (new) Status: PROPOSED Type: refactor / package-split

  • Design: ./design.md
  • Spec deltas: ./specs/font-compress/spec.md, ./specs/font-decompress/spec.md

Why

@suaveplan/font-compress currently ships both BinaryFontPacker and BinaryFontUnpacker from one package — pack and unpack are two distinct concerns wedged together in v0.0.0. The pending @suaveplan/font-decompress row in STATUS.md exists because consumers reasonably expect a decompress-only package: build-time tooling packs fonts once and ships them, while runtime renderers (browsers, R3F scenes, headless test environments) only need to decompress. A renderer pulling the full font-compress payload to use one class drags pack-side code, parse-path utilities, and dictionary builders it never executes. Splitting eliminates that.

Genesis has zero monorepo consumers of @suaveplan/font-compress today — verified via grep -rE "from ['\"]@suaveplan/font-compress" packages --include='*.ts' — so the split has no internal blast radius. External consumers (assuming there are any pre-publish) get a small migration: import BinaryFontUnpacker / createBinaryFontUnpacker from @suaveplan/font-decompress instead of @suaveplan/font-compress. The unpacker's public API is preserved bit-for-bit; only the import path changes.

This is also the pattern the no-deferrals memory captures: the existing font-compress package shipped at 0.0.0 with the decompress half folded in, leaving the named font-decompress row pending — a half-completed identity. The split lands BOTH packages at their full functional surface in one OpenSpec change rather than leaving one in STATUS.md ➖.

What changes

@suaveplan/font-decompress (new package at packages/fonts/font-decompress/)

  • ADDED: package @suaveplan/font-decompress v0.0.0, universal-tier, "sideEffects": false.
  • ADDED: BinaryFontUnpacker class + createBinaryFontUnpacker() factory. Public API is bit-identical to the class currently exported by @suaveplan/font-compress — same unpack(data: Uint8Array): TypefaceFont signature, same wire-format expectations, same TypefaceFont return shape from @suaveplan/font-convert.
  • ADDED: FONT_DECOMPRESS_CODES registry via defineCodes("FONT_DECOMPRESS", [...]) with codes UNPACK_FAILED and INVALID_DATA. FontDecompressError extends SuaveplanError<FontDecompressErrorCode>.
  • ADDED: decodeString(data, offset) utility — moved from font-compress's utils/encode-string.ts. The encode-side helper stays in font-compress.
  • ADDED: BTF format constants module (duplicated from font-compress — see design.md). Both packages share the wire-format constants; duplicating 18 lines of magic numbers avoids a third "fonts-shared" package and keeps each package self-contained.
  • ADDED: Sibling .md docs for every implementation file.
  • ADDED: Subpath exports — ., ./unpacker/binary-font-unpacker, ./error-codes, ./utils/decode-string, ./constants, ./types/index (re-exports TypefaceFont and PathCommand from @suaveplan/font-convert for caller convenience).
  • ADDED: Dependencies: @suaveplan/error, @suaveplan/font-convert, tslib. DevDeps: @suaveplan/testing, vitest, vite, vite-plugin-dts, @vitest/coverage-istanbul, fast-check, typescript, @types/node.

@suaveplan/font-compress (modified, stays at v0.0.0)

  • REMOVED: BinaryFontUnpacker class and createBinaryFontUnpacker factory from the public surface. Their source files move to @suaveplan/font-decompress.
  • REMOVED: src/unpacker/ directory in its entirety.
  • MODIFIED: src/utils/encode-string.ts — decodeString moves to @suaveplan/font-decompress. The file keeps encodeString only.
  • MODIFIED: FONT_COMPRESS_CODES registry — UNPACK_FAILED removed (no longer thrown by any path in this package). PACK_FAILED and INVALID_DATA retained.
  • MODIFIED: Subpath exports — ./unpacker/binary-font-unpacker removed.
  • MODIFIED: Public surface declaration in README.md and src/index.ts.
  • The packer surface (BinaryFontPacker, createBinaryFontPacker), packer error codes, parsePath utility, constants/, and FontCompressError (with the trimmed code registry) remain in font-compress.

Cross-cutting

  • ADDED: New canonical capability font-decompress at openspec/specs/font-decompress/spec.md (created at fold time).
  • MODIFIED: New canonical capability font-compress at openspec/specs/font-compress/spec.md — first canonical spec for the package, capturing the post-split shape.
  • MODIFIED: STATUS.md — flip @suaveplan/font-decompress from – to ✅; update @suaveplan/font-compress description to reflect packer-only scope.
  • MODIFIED: MANIFEST.md — regenerated via bun scripts/folder-reorg/generate-manifest.ts. Both packages appear; font-compress dep count drops by one (decodeString removed); font-decompress lands with the same dep graph as font-compress plus tslib.

Out of scope

  • WOFF / WOFF2 / EOT decompression. Upstream @suaveplan/font-decompress was BTF-format-only (Typeface.js binary), and that's what the package's name has come to mean inside genesis. WOFF/WOFF2 decoding lives in @suaveplan/font-convert already; consumers compose the two when they need to go from .woff2 bytes through TypefaceFont to a rendered glyph. Adding WOFF/WOFF2 to font-decompress would duplicate font-convert. Reason: orthogonal capability, already covered.
  • A third @suaveplan/font-typeface types-only package. The shared TypefaceFont and PathCommand types already live in @suaveplan/font-convert; both packages depend on it. Introducing a third package adds a coordinate without enabling anything new. Reason: existing dep already solves the type-sharing problem.
  • Bumping @suaveplan/font-compress past v0.0.0. CLAUDE.md says packages stay at 0.1.0 until graduation to 1.0.0. The split is a v0.0.0-to-v0.0.0 refactor; no version bump, no CHANGELOG entry. External consumers (none in this monorepo) discover the import-path change via the new spec and the npm registry's package listing. Reason: established versioning policy.

Source mapping

Both packages are sourced from the existing packages/fonts/font-compress/ tree. Specifically:

Source file (current) Destination
packages/fonts/font-compress/src/unpacker/binary-font-unpacker/binary-font-unpacker.ts packages/fonts/font-decompress/src/unpacker/binary-font-unpacker/binary-font-unpacker.ts
packages/fonts/font-compress/src/unpacker/binary-font-unpacker/index.ts packages/fonts/font-decompress/src/unpacker/binary-font-unpacker/index.ts
packages/fonts/font-compress/src/unpacker/binary-font-unpacker/binary-font-unpacker.test.ts packages/fonts/font-decompress/src/unpacker/binary-font-unpacker/binary-font-unpacker.test.ts
packages/fonts/font-compress/src/unpacker/binary-font-unpacker/binary-font-unpacker.md packages/fonts/font-decompress/src/unpacker/binary-font-unpacker/binary-font-unpacker.md
decodeString from src/utils/encode-string.ts new packages/fonts/font-decompress/src/utils/decode-string.ts
src/constants/index.ts (duplicated) packages/fonts/font-decompress/src/constants/index.ts (verbatim copy; 19 lines of format magic)
UNPACK_FAILED and INVALID_DATA from src/error-codes.ts new packages/fonts/font-decompress/src/error-codes.ts (own namespace FONT_DECOMPRESS)

No bit-level changes to algorithm logic. The unpacker's class body, the decodeString helper, and the constants file move verbatim. Imports are updated to point at the new package-local error registry and constants module.

Reference docs consulted

  • ✅ packages/fonts/font-compress/src/ — full read of every TS file (unpacker, packer, utils, constants, error-codes, index)
  • ✅ packages/fonts/font-compress/package.json — exports + deps
  • ✅ ../suaveplan/packages/universal/font-decompress/ — upstream comparison; confirmed BTF-only scope and verified the upstream class is a stripped-down version of what genesis already ships
  • ✅ STATUS.md rows for @suaveplan/font-compress and @suaveplan/font-decompress
  • ✅ Monorepo grep for @suaveplan/font-compress consumers — zero TS importers; only docs reference it

Impact

Affected dependents

None inside the monorepo (verified). External consumers (if any pre-publish) must rewrite imports of BinaryFontUnpacker / createBinaryFontUnpacker / the unpacker/ subpath to point at @suaveplan/font-decompress instead of @suaveplan/font-compress. The class signature and behaviour are unchanged.

Risk

  • Low. Pure code-relocation; algorithm logic untouched. The unpacker's existing 100% test coverage carries forward to the new package along with the source files. The packer's tests in font-compress stay there. New tests verify the moved-in error registry and the trimmed packer-side error registry.
  • Wire-format constants duplicated. If the BTF format ever changes, both packages need updates in lockstep. Documented in design.md; not a problem in practice because the format is locked at v0.0.0.

Migration notes

Single import-path rewrite for any external consumer of the unpacker:

// Before
import { BinaryFontUnpacker, createBinaryFontUnpacker } from "@suaveplan/font-compress";
// After
import { BinaryFontUnpacker, createBinaryFontUnpacker } from "@suaveplan/font-decompress";

Error code consumers that programmatically dispatch on FONT_COMPRESS.UNPACK_FAILED rewrite to FONT_DECOMPRESS.UNPACK_FAILED. Note the namespace change in addition to the package change.