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

101 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```ts
// 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.