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

11 KiB

Compress-SVG completion

Package: @suaveplan/compress-svg Status: PROPOSED Type: spec-conformance + feature

Why

The genesis port of @suaveplan/compress-svg ships only the per-pass SVGO-style plugin core (parser/svg, core/compress-svg, plugins/*, presets, types/common) and a small set of in-place optimisation passes (cleanup-attrs, collapse-groups, convert-path-data, convert-transforms, mangle-ids, remove-comments, remove-doctype, remove-metadata). The legacy package at ../suaveplan/packages/universal/compress-svg/src ships a substantially larger surface that is required by downstream tooling: a high-level SvgOptimizer façade, a structural-flattening pass (SvgStructuralOptimizer), cross-file content-addressed deduplication (SvgDeduplicator), a standalone PathTokenizer module, a CLI bin (suaveplan-svg-optimize), a binary archive format (SvgPacker + SvgUnpacker + SvgArchiveBuilder + SvgArchiveUnpacker totalling ~3 472 lines), and a browser unpacker (SvgUnpackerBrowser + ArchiveUnpacker ~1 592 lines) that consumes the archive without Node.js APIs.

Genesis policy is "no half-baked or mediocre libraries" (CLAUDE.md §Workflow rule 3) and "Preserve the full public surface during rewrites … never trim on 'probably unused' grounds" (CLAUDE.md §Workflow rule 5). The audit on 2026-05-02 confirmed every one of the seven gaps below by file-listing diff between /home/ubuntu/web/suaveplan/packages/universal/compress-svg/src and /home/ubuntu/web/genesis/packages/docs/compress-svg/src. This change closes those gaps in one coordinated pass so the package matches its upstream contract before any consumer wires it up.

What changes

@suaveplan/compress-svg

  • ADDED: SvgOptimizer class — orchestrator that runs the configured plugin pipeline and surfaces a typed OptimizationResult ({ originalBytes, optimizedBytes, pluginsApplied, warnings, durationMs, ratio }); accepts a SvgOptimizerConfig selecting plugins, presets, and the dictionary/structural/dedup pre-passes; exposes optimize(svg) and optimizeBatch(svgs).
  • ADDED: SvgStructuralOptimizer class — flatten redundant <g> wrappers (single-child groups with no styling), collapse identical attribute sets, hoist common attributes to a parent <g>, merge consecutive <path> elements with identical fill/stroke/style, returns a StructuralOptimizationResult with per-rule counts.
  • ADDED: SvgDeduplicator class — cross-file content-addressed deduplication; SHA-256 hashes the canonicalised inner content of repeating subtrees; emits <symbol id="…"> definitions in a hoisted <defs> and replaces duplicates with <use href="#…"/>; returns a DeduplicationReport ({ candidatesScanned, duplicatesFound, bytesSaved, symbolMap }).
  • ADDED: tokenizePath(d) and formatTokens(tokens) — standalone path tokeniser that emits a typed PathToken[] stream ({ command, args, absolute }) and round-trips back to the original d string.
  • ADDED: SvgPacker and SvgUnpacker — binary archive format with version header, SvgArchiveManifest, and content-addressed payload table; pack/unpack by string SVG name → bytes; archive layout matches legacy on-disk format byte-for-byte for cross-tool compatibility.
  • ADDED: ArchiveUnpacker and SvgUnpackerBrowser — browser-only unpackers that read the binary archive without Node.js APIs (Buffer, fs, zlib); use Web Streams and globalThis.crypto; ship under the ./browser subpath export.
  • ADDED: bin/suaveplan-svg-optimize — CLI command that takes glob patterns and writes optimised SVGs (in-place, to a directory, or to a binary archive); flags: --in, --out, --preset, --archive, --dedupe, --structural, --dry-run, --quiet, --json.
  • ADDED: subpath exports ./optimizer, ./structural, ./deduplicator, ./path-tokenizer, ./archive, ./browser, ./cli.
  • ADDED: error codes registered on @suaveplan/error under COMPRESS_SVG.*: OPTIMIZER_PIPELINE_FAILED, STRUCTURAL_INVALID_TREE, DEDUPE_HASH_COLLISION, PATH_TOKEN_PARSE_FAILED, ARCHIVE_VERSION_MISMATCH, ARCHIVE_CHECKSUM_FAILED, ARCHIVE_PAYLOAD_NOT_FOUND, BROWSER_UNSUPPORTED_ENVIRONMENT, CLI_INVALID_GLOB.
  • MODIFIED: package.json bin field — adds suaveplan-svg-optimize. exports map gains the seven new subpaths.

Out of scope

  • Server-side rasterisation to PNG/JPG — belongs in @suaveplan/compress-image; rasterisation requires a different dependency surface (sharp adapter or canvas) and the SVG package must remain a pure-string transformer.
  • Sprite-sheet generation — separate concern (atlas packing, layout, viewBox calc); deferred to a future @suaveplan/svg-sprite package if needed.
  • Lossy colour quantisation beyond the existing plugin set — the existing cleanup-attrs + convert-path-data passes already handle precision; introducing palette quantisation would require a colour-distance algorithm and is not what callers ask for.
  • Drop-in SVGO-plugin compatibility — the genesis plugin model is intentionally simpler than upstream SVGO; mapping arbitrary SVGO plugins is its own change.

Source mapping

This change derives from ../suaveplan/packages/universal/compress-svg/src. There is no dedicated enterprise/compress-svg.md audit doc upstream, so the source mapping cites file:line ranges directly.

Requirement Source file Source lines Notes
SvgOptimizer façade optimizer/SvgOptimizer.ts L1-L483 Verbatim port; replace Node zlib with injectable CompressorAdapter for browser-safe build
OptimizationResult and SvgDictionary types optimizer/SvgOptimizer.types.ts L1-L27 Verbatim
SvgStructuralOptimizer structural/SvgStructuralOptimizer.ts L1-L458 Verbatim
SvgStructuralOptimizer types structural/SvgStructuralOptimizer.types.ts (full file) Verbatim
SvgDeduplicator (cross-file dedup, <use> references, DeduplicationReport) deduplicator/SvgDeduplicator.ts L1-L398 Verbatim port; replace crypto.createHash('sha256') with @suaveplan/crypto-hash sha256 for browser/server parity
tokenizePath / formatTokens / PathToken PathTokenizer.ts L1-L559 Verbatim
SvgPacker / SvgUnpacker (binary archive) SvgPacker.ts L1-L2931 Verbatim port; the on-disk layout matches legacy byte-for-byte for cross-tool compatibility
SvgArchiveBuilder archive/SvgArchiveBuilder.ts L1-L177 Verbatim
SvgArchiveUnpacker archive/SvgArchiveUnpacker.ts L1-L182 Verbatim
ArchiveUnpacker (browser) browser/SvgArchiveUnpackerBrowser.ts L1-L440 Verbatim
SvgUnpackerBrowser browser/SvgUnpackerBrowser.ts L1-L1152 Verbatim
suaveplan-svg-optimize CLI bin cli.ts L1-L271 Verbatim

Requirements with no source line

Requirement Justification
COMPRESS_SVG.* error codes registered on @suaveplan/error Genesis-wide policy that every thrown error route through @suaveplan/error (CLAUDE.md §Workflow rule 2). Upstream throws plain Error.
Replace Node zlib with injectable CompressorAdapter in SvgOptimizer Genesis ships universal-tier packages where browser tier matters; upstream binds to Node zlib directly. The adapter keeps the API identical while enabling the ./browser subpath build.
Replace node:crypto createHash with @suaveplan/crypto-hash sha256 in SvgDeduplicator Same browser/server parity reason; keeps a single SHA-256 implementation across the monorepo.
./optimizer, ./structural, ./deduplicator, ./path-tokenizer, ./archive, ./browser, ./cli subpath exports Genesis-wide subpath-per-feature rule (AGENTS.md §6). Upstream uses a single barrel; tree-shaking parity demands subpath splits.

Verification approach

Each spec delta requirement carries a **Verified by:** line pointing at concrete it(...) titles. The test files that will exist after this change ships:

  • packages/docs/compress-svg/src/optimizer/svg-optimizer.test.ts — pipeline orchestration, plugin selection, OptimizationResult shape, batch mode
  • packages/docs/compress-svg/src/optimizer/svg-optimizer.spec-conformance.test.ts — injectable CompressorAdapter round-trip; pipeline-failed error code; OTel histogram
  • packages/docs/compress-svg/src/structural/svg-structural-optimizer.test.ts — flatten single-child <g>; collapse identical attribute sets; hoist common attrs; merge consecutive paths
  • packages/docs/compress-svg/src/structural/svg-structural-optimizer.spec-conformance.test.ts — invalid-tree error code; per-rule counter accuracy
  • packages/docs/compress-svg/src/deduplicator/svg-deduplicator.test.ts — cross-file dedup, <symbol>/<use> emission, hash-collision rejection
  • packages/docs/compress-svg/src/deduplicator/svg-deduplicator.spec-conformance.test.ts — DeduplicationReport shape; injected hash function honoured; collision error code
  • packages/docs/compress-svg/src/path-tokenizer/path-tokenizer.test.ts — every SVG path command (MmLlHhVvCcSsQqTtAaZz); round-trip property test
  • packages/docs/compress-svg/src/path-tokenizer/path-tokenizer.spec-conformance.test.ts — malformed-path error code; absolute/relative flag preserved
  • packages/docs/compress-svg/src/archive/svg-packer.test.ts — binary header, manifest, payload table, content-addressed dedup, version round-trip
  • packages/docs/compress-svg/src/archive/svg-packer.spec-conformance.test.ts — version-mismatch error; checksum-failed error; payload-not-found error
  • packages/docs/compress-svg/src/browser/svg-unpacker-browser.test.ts — browser unpacker round-trip with the same fixtures as svg-packer.test.ts
  • packages/docs/compress-svg/src/browser/svg-unpacker-browser.spec-conformance.test.ts — Node Buffer/fs/zlib not referenced (static import scan); BROWSER_UNSUPPORTED_ENVIRONMENT thrown when globalThis.crypto is missing
  • packages/docs/compress-svg/src/cli/cli.test.ts — argument parsing, glob expansion, in-place vs out-dir vs archive modes, dry-run, JSON reporter
  • packages/docs/compress-svg/src/cli/cli.spec-conformance.test.ts — invalid-glob error code; exit codes match POSIX conventions
  • tests/dep-graph.test.ts — no Node.js API import on the ./browser subpath; subpath exports resolve

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/compress-svg to enumerate. Result of grep -rl "@suaveplan/compress-svg" /home/ubuntu/web/genesis/packages/ --include="package.json" on 2026-05-02 returned only the package itself (packages/docs/compress-svg/package.json). No in-repo dependents.

  • No dependents (verified by grep -rl "@suaveplan/compress-svg" /home/ubuntu/web/genesis/packages/ --include="package.json" returning only the package's own package.json).

This change is additive only — every existing export is preserved, and the new exports land under new subpaths (./optimizer, ./structural, ./deduplicator, ./path-tokenizer, ./archive, ./browser, ./cli) plus root-level re-exports. The CLI bin is a new entry; existing users who depend on the per-pass plugin core continue to compile unchanged.