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:
SvgOptimizerclass — orchestrator that runs the configured plugin pipeline and surfaces a typedOptimizationResult({ originalBytes, optimizedBytes, pluginsApplied, warnings, durationMs, ratio }); accepts aSvgOptimizerConfigselecting plugins, presets, and the dictionary/structural/dedup pre-passes; exposesoptimize(svg)andoptimizeBatch(svgs). - ADDED:
SvgStructuralOptimizerclass — 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 aStructuralOptimizationResultwith per-rule counts. - ADDED:
SvgDeduplicatorclass — 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 aDeduplicationReport({ candidatesScanned, duplicatesFound, bytesSaved, symbolMap }). - ADDED:
tokenizePath(d)andformatTokens(tokens)— standalone path tokeniser that emits a typedPathToken[]stream ({ command, args, absolute }) and round-trips back to the originaldstring. - ADDED:
SvgPackerandSvgUnpacker— 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:
ArchiveUnpackerandSvgUnpackerBrowser— browser-only unpackers that read the binary archive without Node.js APIs (Buffer,fs,zlib); use Web Streams andglobalThis.crypto; ship under the./browsersubpath 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/errorunderCOMPRESS_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.jsonbinfield — addssuaveplan-svg-optimize.exportsmap 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 (sharpadapter 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-spritepackage if needed. - Lossy colour quantisation beyond the existing plugin set — the existing
cleanup-attrs+convert-path-datapasses 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,OptimizationResultshape, batch modepackages/docs/compress-svg/src/optimizer/svg-optimizer.spec-conformance.test.ts— injectableCompressorAdapterround-trip; pipeline-failed error code; OTel histogrampackages/docs/compress-svg/src/structural/svg-structural-optimizer.test.ts— flatten single-child<g>; collapse identical attribute sets; hoist common attrs; merge consecutive pathspackages/docs/compress-svg/src/structural/svg-structural-optimizer.spec-conformance.test.ts— invalid-tree error code; per-rule counter accuracypackages/docs/compress-svg/src/deduplicator/svg-deduplicator.test.ts— cross-file dedup,<symbol>/<use>emission, hash-collision rejectionpackages/docs/compress-svg/src/deduplicator/svg-deduplicator.spec-conformance.test.ts—DeduplicationReportshape; injected hash function honoured; collision error codepackages/docs/compress-svg/src/path-tokenizer/path-tokenizer.test.ts— every SVG path command (MmLlHhVvCcSsQqTtAaZz); round-trip property testpackages/docs/compress-svg/src/path-tokenizer/path-tokenizer.spec-conformance.test.ts— malformed-path error code; absolute/relative flag preservedpackages/docs/compress-svg/src/archive/svg-packer.test.ts— binary header, manifest, payload table, content-addressed dedup, version round-trippackages/docs/compress-svg/src/archive/svg-packer.spec-conformance.test.ts— version-mismatch error; checksum-failed error; payload-not-found errorpackages/docs/compress-svg/src/browser/svg-unpacker-browser.test.ts— browser unpacker round-trip with the same fixtures assvg-packer.test.tspackages/docs/compress-svg/src/browser/svg-unpacker-browser.spec-conformance.test.ts— NodeBuffer/fs/zlibnot referenced (static import scan);BROWSER_UNSUPPORTED_ENVIRONMENTthrown whenglobalThis.cryptois missingpackages/docs/compress-svg/src/cli/cli.test.ts— argument parsing, glob expansion, in-place vs out-dir vs archive modes, dry-run, JSON reporterpackages/docs/compress-svg/src/cli/cli.spec-conformance.test.ts— invalid-glob error code; exit codes match POSIX conventionstests/dep-graph.test.ts— no Node.js API import on the./browsersubpath; 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 ownpackage.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.