152 lines
7.1 KiB
Markdown
152 lines
7.1 KiB
Markdown
# Design — split-font-compress
|
|
|
|
Non-normative. File layout for the two packages after the split, the rationale for duplicating the BTF wire-format constants, and the boundary between pack and unpack error registries.
|
|
|
|
## Post-split file layout
|
|
|
|
### `packages/fonts/font-decompress/`
|
|
|
|
```text
|
|
packages/fonts/font-decompress/
|
|
package.json # @suaveplan/font-decompress v0.0.0, sideEffects: false
|
|
tsconfig.json
|
|
tsconfig.lib.json
|
|
vite.config.ts
|
|
vitest.config.ts
|
|
README.md
|
|
src/
|
|
index.ts # explicit named re-exports
|
|
constants/
|
|
index.ts # CMD_M, CMD_L, CMD_B, CMD_Q, CMD_Z, CMD_COORD_COUNTS, METRICS_COUNT
|
|
error-codes.ts # FONT_DECOMPRESS_CODES + FontDecompressError
|
|
error-codes.test.ts
|
|
error-codes.md
|
|
utils/
|
|
decode-string.ts # decodeString(data, offset)
|
|
decode-string.test.ts
|
|
decode-string.md
|
|
unpacker/
|
|
binary-font-unpacker/
|
|
binary-font-unpacker.ts # BinaryFontUnpacker class + createBinaryFontUnpacker factory
|
|
binary-font-unpacker.test.ts
|
|
binary-font-unpacker.md
|
|
index.ts
|
|
types/
|
|
index.ts # re-exports TypefaceFont, PathCommand from @suaveplan/font-convert
|
|
```
|
|
|
|
### `packages/fonts/font-compress/` (post-trim)
|
|
|
|
```text
|
|
packages/fonts/font-compress/
|
|
package.json # unchanged version, subpath exports trimmed
|
|
README.md # updated to reflect packer-only scope
|
|
src/
|
|
index.ts # drops BinaryFontUnpacker / createBinaryFontUnpacker exports
|
|
constants/
|
|
index.ts # unchanged (CMD_*, CMD_COORD_COUNTS, METRICS_COUNT)
|
|
error-codes.ts # FONT_COMPRESS_CODES trimmed: { PACK_FAILED, INVALID_DATA }
|
|
utils/
|
|
encode-string.ts # encodeString only; decodeString removed
|
|
parse-path.ts # unchanged
|
|
packer/
|
|
binary-font-packer/
|
|
binary-font-packer.ts
|
|
binary-font-packer.test.ts
|
|
binary-font-packer.md
|
|
index.ts
|
|
# NOTE: src/unpacker/ removed entirely
|
|
```
|
|
|
|
## Why duplicate the BTF constants rather than introducing a shared package
|
|
|
|
The BTF wire-format constants are 19 lines of magic numbers (`CMD_M = 0`, `CMD_L = 1`, etc.). Three alternatives were considered:
|
|
|
|
1. **Duplicate (chosen).** Each package keeps its own copy of `constants/index.ts`. Trivially low maintenance; if the format ever changes, both packages get updated in the same commit (which is when wire-format changes happen anyway — they are coordinated breaking events).
|
|
2. **Move to a third `@suaveplan/font-typeface-format` package.** Adds a coordinate package with three constants and no functional behaviour. Costs a `workspace:^` dep on both packages plus another row in `MANIFEST.md` for marginal benefit.
|
|
3. **Make `font-compress` depend on `font-decompress` for the constants.** Asymmetric — compress conceptually generates what decompress reads, so a forward dep would invert the natural composition. Rejected.
|
|
|
|
Option 1 is the cleanest. The duplication is auditable in one place per package, both files are byte-identical, and `bun scripts/folder-reorg/generate-manifest.ts` confirms each package's dep count stays minimal.
|
|
|
|
## Error registry split
|
|
|
|
`font-compress` originally shipped three codes: `PACK_FAILED`, `UNPACK_FAILED`, `INVALID_DATA`. Post-split:
|
|
|
|
| Code | font-compress | font-decompress | Notes |
|
|
|---|---|---|---|
|
|
| `PACK_FAILED` | ✅ kept | — | Thrown by `BinaryFontPacker.pack` only |
|
|
| `UNPACK_FAILED` | ❌ removed | ✅ added | Thrown by `BinaryFontUnpacker.unpack` only |
|
|
| `INVALID_DATA` | ✅ kept | ✅ added | Both pack and unpack throw it on boundary-validation failures; namespaces keep the codes distinct |
|
|
|
|
The namespaces (`FONT_COMPRESS` vs `FONT_DECOMPRESS`) ensure that even codes with the same reason string are programmatically distinct:
|
|
|
|
- `FONT_COMPRESS.INVALID_DATA` — pack input was malformed
|
|
- `FONT_DECOMPRESS.INVALID_DATA` — unpack input was malformed
|
|
|
|
Consumers dispatching on the error code see different strings; consumers using `err instanceof FontCompressError` vs `err instanceof FontDecompressError` see different classes.
|
|
|
|
## Public-surface diffs
|
|
|
|
### `font-compress/src/index.ts` (post-split)
|
|
|
|
```ts
|
|
export type { FontCompressErrorCode } from "./error-codes.js";
|
|
export { FONT_COMPRESS_CODES, FontCompressError } from "./error-codes.js";
|
|
export {
|
|
BinaryFontPacker,
|
|
createBinaryFontPacker,
|
|
} from "./packer/binary-font-packer/index.js";
|
|
// REMOVED: BinaryFontUnpacker + createBinaryFontUnpacker exports.
|
|
```
|
|
|
|
### `font-decompress/src/index.ts` (new)
|
|
|
|
```ts
|
|
export type { FontDecompressErrorCode } from "./error-codes.js";
|
|
export { FONT_DECOMPRESS_CODES, FontDecompressError } from "./error-codes.js";
|
|
export {
|
|
BinaryFontUnpacker,
|
|
createBinaryFontUnpacker,
|
|
} from "./unpacker/binary-font-unpacker/index.js";
|
|
export type { PathCommand, TypefaceFont } from "@suaveplan/font-convert";
|
|
```
|
|
|
|
The convenience re-export of `TypefaceFont` and `PathCommand` from `@suaveplan/font-convert` lets consumers type their `unpack(...)` return value without taking an extra dep — but it is a single named re-export of types only, which CLAUDE.md rule §4 permits (the ban is on convenience re-exports of external runtime deps, not on types).
|
|
|
|
Actually — rule §4 says "NEVER re-export external deps for convenience — import directly from source." That applies here too. Removing the convenience re-export; consumers import `TypefaceFont` directly from `@suaveplan/font-convert`. The package's own types module just declares its public shapes.
|
|
|
|
Revised `font-decompress/src/index.ts`:
|
|
|
|
```ts
|
|
export type { FontDecompressErrorCode } from "./error-codes.js";
|
|
export { FONT_DECOMPRESS_CODES, FontDecompressError } from "./error-codes.js";
|
|
export {
|
|
BinaryFontUnpacker,
|
|
createBinaryFontUnpacker,
|
|
} from "./unpacker/binary-font-unpacker/index.js";
|
|
```
|
|
|
|
No type re-export. `TypefaceFont` and `PathCommand` come from `@suaveplan/font-convert` directly.
|
|
|
|
## Test coverage strategy
|
|
|
|
- `font-decompress` inherits the unpacker's existing 100% test file. The tests get moved verbatim alongside the implementation; only the import paths update. New tests cover the package's own `error-codes.ts` (every code reachable) and `decode-string.ts` (byte-level boundary cases).
|
|
- `font-compress` keeps its packer test file untouched. Its `error-codes.test.ts` is regenerated to assert the trimmed registry (no `UNPACK_FAILED`). The `encode-string.test.ts` is trimmed to drop the `decodeString` cases that move to `font-decompress`.
|
|
|
|
## Build/dist verification
|
|
|
|
Both packages run their gates independently:
|
|
|
|
```bash
|
|
bunx turbo lint typecheck test --filter=@suaveplan/font-compress
|
|
bunx turbo lint typecheck test --filter=@suaveplan/font-decompress
|
|
bunx turbo test:coverage --filter=@suaveplan/font-compress
|
|
bunx turbo test:coverage --filter=@suaveplan/font-decompress
|
|
bun run docs:check --filter=@suaveplan/font-compress
|
|
bun run docs:check --filter=@suaveplan/font-decompress
|
|
bunx turbo build --filter=@suaveplan/font-compress
|
|
bunx turbo build --filter=@suaveplan/font-decompress
|
|
```
|
|
|
|
All eight must pass before archive. Coverage thresholds are 100% Istanbul on both packages.
|