Skip to content

refactor!: v4 — restructure library internals, drop v2 emit shim - #9

Merged
kjanat merged 15 commits into
masterfrom
refactor/v4-src-restructure
Jun 30, 2026
Merged

refactor!: v4 — restructure library internals, drop v2 emit shim#9
kjanat merged 15 commits into
masterfrom
refactor/v4-src-restructure

Conversation

@kjanat

@kjanat kjanat commented Jun 30, 2026

Copy link
Copy Markdown
Owner

What & why

Restructures src/*.ts (library internals; CLI rewired only where shared code moved) from a 603-line closure god-file + dual tag models + fake-constraint types into focused, pipeline-stage modules. Ships as 4.0.0 with the v2 emit shim removed.

The three problems this fixes:

  1. index.ts was a 603-line closure-as-object — 9 mutable vars + ~12 inner functions, none testable in isolation. Now 298 lines of Vite glue over an extracted, testable AssetProducer.
  2. The favicon-tag model was built twice (plugin's private faviconTags() vs html.ts's buildFaviconTags(), with withBase duplicated). Now one buildFaviconTags(injections, ctx) serves both plugin and CLI.
  3. Parsing was smeared across 3 sites and IconSize was a fake constraint ((number & {}) accepts anything). Now one parseConfig() boundary; IconSize is a branded type minted only by parseSize().

New shape (by pipeline stage)

Stage Modules
parse config.ts · size.ts · load-input.ts
resolve resolve-specs.ts
produce raster.ts (sharp) · ico.ts (packing) · assets.ts (AssetProducer) · data-uri.ts
render favicon-tags.ts (one builder) · inject-html.ts · dev-client.ts
assemble index.ts (thin) · types.ts

Deleted: normalize-emit.ts, html.ts. Net −228 lines in the library.

BREAKING (v4)

  • emit accepts only EmitSpec[] — the v2 { source, sizes, inject } object shape is removed.
  • Removed exports: LegacyEmitOptions, EmitOptions, isLegacyEmit, NormalizedEmit, IncludeSourceOptions, EmitSizesFormat, EMIT_SIZES_FORMATS, InjectMode, INJECT_MODES.
  • Removed the inert svg-to-ico inject --mode/-m flag (never affected output).

Migration table in CHANGELOG.md + README.md.

Verification

  • 176 tests pass / 0 fail (suite green at every one of the 9 commits)
  • typecheck · biome lint · dprint — all clean
  • tsdown build passes attw (types) + publint (packaging)

Each commit is a self-contained, green phase — reviewable in order.

The v4.0.0 signed tag is intentionally not in this branch (per repo convention, tag the work commit after merge).

kjanat added 11 commits June 30, 2026 00:11
Plugin emit specs gain `emit` + `inject: 'embed'` (+ SvgSpec
`encoding`) to inline ICO/PNG/SVG bytes into the HTML `<link>`
instead of, or alongside, writing files. `svg-to-ico inject`
mirrors it with `--embed` / `--encoding` / `--asset-dir`. New
pure `src/data-uri.ts`; data hrefs skip cache-busting and the
dev HMR client; no-output specs warn once.

Also migrate CLI help text to ANSI colors via `colors.ts` (TTY
+ NO_COLOR/FORCE_COLOR guard, auto-reset) and drop a redundant
`as InjectMode`. Rides along with already-staged build/CI
tweaks (dprint, autofix workflow, bunfig, preload removal).
BREAKING: `emit` accepts only `EmitSpec[]`; removed the v2
`{ source, sizes, inject }` object shape and its exported types
(LegacyEmitOptions, EmitOptions, isLegacyEmit, NormalizedEmit,
IncludeSourceOptions, EmitSizesFormat, EMIT_SIZES_FORMATS).

All option parsing + validation collapses into one pure
`parseConfig(opts) -> ResolvedConfig` boundary; the config hook
only applies Vite root/base. normalize-emit.ts deleted.
Byte production + caching + embed-URI memoization move into a
testable AssetProducer class; dev client snippets move to
dev-client.ts. index.ts drops from 603 to ~298 lines and only
wires parseConfig → resolveSpecs → producer/builder into the
three Vite plugins. Also removes the `as { size }` assertions
via clean discriminated narrowing.
Public option size fields are plain `number`; the validated
`IconSize` brand is produced only by `parseSize(raw, max)` in
resolve-specs as it builds the internal size structures.
Bump to 4.0.0, CHANGELOG release notes with the v2→v3 migration
table, README updated to EmitSpec[]-only.
The `--mode`/`-m` flag never affected output (the CLI emits ICO
+ optional SVG links regardless of minimal/full). Drops it plus
the now-dead `InjectMode` / `INJECT_MODES` exports.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 12a6ddab-c309-4246-acd9-b6f025a3bc53

📥 Commits

Reviewing files that changed from the base of the PR and between 33523de and 531f1fd.

📒 Files selected for processing (2)
  • src/data-uri.ts
  • tests/data-uri.test.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Socket Security: Pull Request Alerts
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-05-12T20:04:37.791Z
Learnt from: kjanat
Repo: kjanat/vite-svg-to-ico PR: 7
File: tests/plugin.test.ts:24-32
Timestamp: 2026-05-12T20:04:37.791Z
Learning: For Vite plugins, the `configResolved` hook must return `void` or `Promise<void>` (per Vite’s `ObjectHook` type). Do not return boolean values from `configResolved`. If you need boolean control flow, use the appropriate hook types (e.g., other hooks like `config`/`configureServer`/`transformIndexHtml` may support different return shapes) rather than returning a boolean from `configResolved`.

Applied to files:

  • tests/data-uri.test.ts
  • src/data-uri.ts
🔍 Remote MCP GitHub Grep

Additional review context

  • data:image/svg+xml,... with URI-escaped SVG is a common pattern in public code, including uses that rely on percent-encoding rather than raw SVG text. Examples found in n8n, microsoft/vscode, beepbox, unionlabs/union, and deck.gl.
  • Cache-busting via url.searchParams.set('v', ...) is also common in public code (e.g. travis-web, Ghost, nextcloud, directus, GitBook, OctoberCMS).
  • Favicon replacement commonly targets rel="shortcut icon" / rel="icon" specifically and often leaves apple-touch-icon alone; examples include roundcubemail, senna.js, firefox-ios, and markuplint. Some implementations explicitly replace the <link> element rather than mutating only href.
  • A concrete public example (roundcubemail) notes that simply changing href may be insufficient in some browsers, so replacing the favicon <link> element is used instead.
🔇 Additional comments (2)
tests/data-uri.test.ts (1)

65-77: LGTM!

src/data-uri.ts (1)

38-53: LGTM!


📝 Walkthrough

Refactor vite-svg-to-ico internals into a clearer multi-stage pipeline and prepare v4.0.0.

  • Break the previous closure-heavy src/index.ts into focused modules: parseConfig() (single options→resolved boundary), size branding/validation (parseSize), raster/ICO generation (raster, ico), spec resolution (resolveSpecs), shared favicon tag building (buildFaviconTags), HTML injection utilities (inject-html), and cached asset production (AssetProducer), plus data: URI helpers.
  • Unify favicon <link> tag generation between the Vite plugin and CLI via the shared builder, including consistent cache-busting and dev HMR that skips data: URIs.
  • Remove the v2 emit compatibility shim; emit now accepts only EmitSpec[], and delete the legacy emit normalisation/types and related exports.
  • Remove the inert svg-to-ico inject --mode / -m flag and associated legacy InjectMode/INJECT_MODES exports.
  • Add embedding support as data: URIs: inject: 'embed' (incl. SVG SvgSpec.encoding with base64 default / utf8), PNG embed options, and CLI support via svg-to-ico inject --embed --encoding --asset-dir.
  • Update public docs and migration guidance in README.md/CHANGELOG.md for the v4 emit: EmitSpec[] contract, embedding behaviour, and embed/encoding flags; bump package version to 4.0.0.
  • Refresh tests and CI/dev ergonomics: add targeted unit suites for the new modules (config, data-uri, favicon-tags, inject-html, asset producer, raster), update integration/plugin expectations for the new emit array model, add a pretest pack+install step for smoke tests, and adjust lint/format/coverage and fixture ignores.

Verification reported: 176 passing tests, clean typecheck/lint/format, and successful tsdown checks (attw, publint).

Walkthrough

This PR bumps the package to v4.0.0, removes the legacy v2 emit shape, and splits the plugin into shared modules for config parsing, raster generation, asset production, spec resolution, favicon tag building, HTML injection, and dev-time shim/HMR handling. It adds inline data: URI embedding for emitted assets, including SVG encoding control and new CLI inject flags for embedding and asset directory selection. Tests, docs, and project tooling are updated to match the new API and behaviour.

Sequence Diagram(s)

sequenceDiagram
  participant Vite
  participant svgToIco
  participant parseConfig
  participant AssetProducer
  participant buildFaviconTags
  participant buildShimScript

  Vite->>svgToIco: initialise plugin
  svgToIco->>parseConfig: parseConfig(opts)
  parseConfig-->>svgToIco: ResolvedConfig
  svgToIco->>AssetProducer: new AssetProducer(cfg, sizes)
  Vite->>svgToIco: transformIndexHtml / build hooks
  svgToIco->>buildFaviconTags: buildFaviconTags(injections, ctx)
  buildFaviconTags-->>svgToIco: HtmlTagDescriptor[]
  svgToIco->>buildShimScript: buildShimScript(tags, hmr)
  buildShimScript-->>svgToIco: shim script
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • kjanat/vite-svg-to-ico#6: Closely related to the HTML favicon injection path that this PR splits into shared helpers and updates for embedding.
  • kjanat/vite-svg-to-ico#7: Closely related to the v3 emit-spec pipeline that this PR extends into the v4-only model with embed/data-URI support.

Suggested labels

enhancement

Poem

🏴‍☠️ The old v2 crate got tossed into the brine,
New modules snap together, neat and fine.
data: flags now flash like treasure in the foam,
And v4 sails in, grumpy but at home.

🚥 Pre-merge checks | ✅ 5 | ❌ 3

❌ Failed checks (3 inconclusive)

Check name Status Explanation Resolution
Changelog Update ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Semver Version Bump Validation ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Agents.Md Documentation Updated ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the v4 refactor and removal of the v2 emit shim, matey.
Description check ✅ Passed The description matches the changeset, covering the module refactor, shared builders, and v4 breaking changes.
Docstring Coverage ✅ Passed Docstring coverage is 96.43% which is sufficient. The required threshold is 30.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

The smoke fixture is no longer a workspace member resolving to a
volatile (clean-on-build) dist/. A `pretest` lifecycle script
packs the plugin to a tarball inside the fixture and installs it,
so CI's `test` step builds+packs before running — the smoke test
exercises the real published artifact. The standalone consumer
fixture is excluded from the library typecheck.
@kjanat kjanat self-assigned this Jun 30, 2026
@kjanat kjanat added the cr:review Allow CodeRabbit review label Jun 30, 2026
@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jun 30, 2026
coderabbitai[bot]

This comment was marked as resolved.

- config: validate emit/inject/encoding shapes and dev flags at the parse
  boundary; reject empty PNG inject.sizes subsets (silent no-output)
- favicon-tags: fragment-aware cacheBust (?v= before #frag); widen embed
  resolver return type to Promise<string | undefined>
- dev-client: HMR re-busts only icon/shortcut-icon links, not apple-touch-icon
- raster: reject ICO layers above 256px before packing
- cli inject: preload embed assets from resolved injections, not --source
- tests: cover the new validation, cacheBust fragments, ICO range guard;
  await fixture restores in integration finally blocks

Skipped (deliberate): autofix.yml hard-fail (dismissed on PR), data-uri SVG
escaping (intentional mini-svg-data-uri behavior, test-covered), raster path
passthrough (CR's suggestion regresses to N reads for N sizes).
@coderabbitai coderabbitai Bot added the documentation Improvements or additions to documentation label Jun 30, 2026
coderabbitai[bot]

This comment was marked as resolved.

CI: the smoke fixture's bun.lock pinned the packed tarball's integrity, but
pretest repacks it from changing source every run, so CI's CI=true (implicit
frozen) install hit IntegrityCheckFailed. Stop tracking the fixture lockfile
and install it --no-frozen-lockfile so it reconciles to the fresh tarball.

data-uri: escapeSvgUtf8 swapped " -> ' and collapsed whitespace, mutating
CDATA / xml:space="preserve" / <style> content so the embedded favicon could
render differently from source. Now percent-encode only what the URI/attribute
requires; the SVG round-trips byte-for-byte.

raster: generateIco also rejects sizes below 1 (was >256 only); await the
rejection assertion in its test (both per CodeRabbit).

README: document the v4 inject:'embed' / encoding feature, fix PngSpec sizes
(1-4096, not 1-256), add emit/encoding rows to the spec tables.
@coderabbitai coderabbitai Bot removed the documentation Improvements or additions to documentation label Jun 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/data-uri.ts`:
- Around line 39-46: The escapeSvgUtf8 helper in src/data-uri.ts is still
leaving raw carriage returns unencoded, which breaks the byte-for-byte
round-trip for Windows-style line endings. Update escapeSvgUtf8 to
percent-encode \r alongside the existing percent-encoding logic, and add a
regression test in the data URI/SVG test coverage that includes a \r\n case to
verify the href preserves the original bytes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 85c3bd59-6b8f-49e3-ad26-b6bf7e112396

📥 Commits

Reviewing files that changed from the base of the PR and between 863fd16 and 33523de.

⛔ Files ignored due to path filters (1)
  • tests/smoke/fixture/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • .gitignore
  • README.md
  • package.json
  • src/data-uri.ts
  • src/raster.ts
  • tests/cli.test.ts
  • tests/data-uri.test.ts
  • tests/raster.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: Socket Security: Pull Request Alerts
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (actions)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-05-12T20:04:37.791Z
Learnt from: kjanat
Repo: kjanat/vite-svg-to-ico PR: 7
File: tests/plugin.test.ts:24-32
Timestamp: 2026-05-12T20:04:37.791Z
Learning: For Vite plugins, the `configResolved` hook must return `void` or `Promise<void>` (per Vite’s `ObjectHook` type). Do not return boolean values from `configResolved`. If you need boolean control flow, use the appropriate hook types (e.g., other hooks like `config`/`configureServer`/`transformIndexHtml` may support different return shapes) rather than returning a boolean from `configResolved`.

Applied to files:

  • tests/raster.test.ts
  • src/data-uri.ts
  • tests/cli.test.ts
  • tests/data-uri.test.ts
  • src/raster.ts
🪛 LanguageTool
README.md

[uncategorized] ~277-~277: Loose punctuation mark.
Context: ... | | inject | boolean \| 'embed' \| { sizes?, embed? } | false | `tru...

(UNLIKELY_OPENING_PUNCTUATION)

🔍 Remote MCP GitHub Grep

Relevant external code patterns found:

  • SVG data URIs are commonly generated with data:image/svg+xml,... plus encodeURIComponent(svg); I found this in adobe/react-spectrum, mapbox/geojson.io, graphif/project-graph, and others. Some code also uses the charset=utf-8 variant.
  • Base64 SVG embedding is also used in public code, e.g. data:image/svg+xml;base64, via btoa(unescape(encodeURIComponent(svg))) in anyproto/anytype-ts and related repos.
  • Cache-busting with url.searchParams.set('v', ...) is a common pattern in public code, including travis-web, directus, nextcloud, and Ghost.
  • Favicon handling commonly targets rel="shortcut icon" / rel="icon" separately from apple-touch-icon; examples include roundcubemail, firefox-ios, senna.js, and markuplint. Some code explicitly replaces the whole <link rel="shortcut icon"> element rather than just mutating href.

Comment thread src/data-uri.ts Outdated
The WHATWG URL parser strips raw \t/\n/\r from any URL (HTML normalizes
CR/CRLF to LF before that), so an unencoded line ending silently vanished
from the decoded SVG — breaking the byte-for-byte round-trip for multi-line
or CRLF sources. CodeRabbit flagged \r; \n and \t fail identically. Encode
all three, and add a regression that decodes through `new URL()` (not just
`decodeURIComponent`, which never exercised the stripping).
@kjanat
kjanat merged commit 23fcc87 into master Jun 30, 2026
8 of 9 checks passed
@kjanat
kjanat deleted the refactor/v4-src-restructure branch June 30, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cr:review Allow CodeRabbit review enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant