Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dprint.jsonc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{ // git ls-remote "https://github.com/kjanat/kjanat.git" HEAD | awk '{print substr($1, 1, 8)}' | xargs -r -I{} sed -i -E 's#(github\.com/kjanat/kjanat/raw/)[0-9a-zA-Z]{4,40}(/configs/dprint\.remote\.json)#\1{}\2#g' .dprint.jsonc
"lineWidth": 120,
"extends": "https://github.com/kjanat/kjanat/raw/e5f1f678/configs/dprint.remote.json",
"excludes": [".github/workflows/capture.yml", ".github/actions/capture/action.yml", "**/tests/fixtures"],
"excludes": ["tests/fixtures", "tests/*/fixture"],
"malva": { "useTabs": false },
"markup": {
"indentWidth": 2,
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ jobs:
- uses: kjanat/runner@master
- uses: oven-sh/setup-bun@v2
- run: runner install fmt:autofix
- if: github.ref_name == 'master'
- if: github.ref_name == 'master' && github.event_name == 'push'
run: run fmt:cfg:up
- if: github.ref_name == 'master' && github.event_name == 'push'
run: |
git ls-remote "https://github.com/kjanat/kjanat.git" HEAD | awk '{print substr($1, 1, 8)}' \
| xargs -r -I{} sed -i -E 's#(github\.com/kjanat/kjanat/raw/)[0-9a-zA-Z]{4,40}(/configs/dprint\.remote\.json)#\1{}\2#g' \
.dprint.jsonc
Comment thread
kjanat marked this conversation as resolved.
- uses: autofix-ci/action@v1
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ web_modules/
# Output of 'npm pack'
*.tgz

# Smoke-fixture lockfile: pins the integrity of the packed tarball, which is
# rebuilt from changing source on every `pretest` run. Tracking it would drift
# and break CI's frozen install — the fixture regenerates it each run instead.
tests/smoke/fixture/bun.lock

# Yarn Integrity file
.yarn-integrity

Expand Down Expand Up @@ -161,3 +166,4 @@ dist
AGENTS.md
/_*.md
/.idea/
superpowers/
5 changes: 5 additions & 0 deletions .markdownlint.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"MD013": false,
"MD033": false,
"MD041": false
}
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [4.0.0] - 2026-06-30

### Removed (BREAKING)

- Dropped the v2 `emit: { source, sizes, inject }` object shape — `emit` now
accepts only an `EmitSpec[]` array. Removed the exported types
`LegacyEmitOptions`, `EmitOptions`, `isLegacyEmit`, `NormalizedEmit`,
`IncludeSourceOptions`, `EmitSizesFormat`, and `EMIT_SIZES_FORMATS`.
- Removed the inert `--mode`/`-m` flag from the `svg-to-ico inject` CLI and the
now-unused `InjectMode` / `INJECT_MODES` exports — the flag never affected
output (the CLI emits ICO + optional SVG links regardless).

| v2 (removed) | v3/v4 |
| ------------------------------------------- | -------------------------------------------------------------------------- |
| `emit: { source: true }` | `emit: [{ format: 'ico' }, { format: 'svg' }]` |
| `emit: { sizes: 'png' }` | `emit: [{ format: 'ico' }, { format: 'png', sizes: [16, 32, 48] }]` |
| `emit: { sizes: 'ico' }` | `emit: [{ format: 'ico' }, { format: 'ico', sizes: [n], filename: … }, …]` |
| `emit: { source: true, inject: 'minimal' }` | `emit: [{ format: 'ico', inject: true }, { format: 'svg', inject: true }]` |
| `emit: { inject: 'full', sizes: 'png' }` | add `{ format: 'png', sizes: […], inject: true }` to the array |

### Internal

- Restructured the library internals (`src/*.ts`) by pipeline stage: a single
`parseConfig` boundary (`config.ts`) replaces three scattered parse/validate
sites; one shared favicon-tag builder (`favicon-tags.ts`) serves both the
plugin and the CLI (removing the duplicated `withBase`/`<link>` logic); byte
production moves into a testable `AssetProducer` (`assets.ts`); `index.ts`
shrinks from ~600 to ~300 lines. `ico.ts` split into `raster.ts` (sharp) +
`ico.ts` (packing); `html.ts` split into `favicon-tags.ts` + `inject-html.ts`.
`IconSize` is now a branded type produced by `parseSize` at the boundary
(public option fields remain plain `number`). No runtime behavior change
beyond the v2 removal above.

### Added

- Embed favicons inline as `data:` URIs instead of (or alongside) emitting
files. Each `ico`/`png`/`svg` emit spec gains two orthogonal knobs:
- `inject: 'embed'` — the injected `<link>`'s `href` carries the image
bytes as a `data:` URI (base64 for binary, configurable for SVG) rather
than pointing at a file.
- `emit: false` — skip writing the file to disk; only meaningful with
`inject: 'embed'` (embed without a file). Defaults to `true`.
- `SvgSpec.encoding: 'base64' | 'utf8'` — `utf8` produces a smaller,
human-readable `data:image/svg+xml,…` URI. Defaults to `'base64'`.
- PNG specs also accept `{ sizes, embed: true }` to inline a subset.

Data-URI hrefs are never cache-busted (a query param would corrupt the
bytes), and the dev HMR client skips them. A spec that writes nothing and
injects nothing now emits a one-time config warning.

- `svg-to-ico inject` gained matching `--embed` / `--encoding` / `--asset-dir`
flags: inline the referenced ICO (and SVG `--source`) straight into the
rewritten HTML as `data:` URIs instead of URL `<link href>`s. Assets are
read from `--asset-dir` (default: each HTML file's own directory).

### Changed

- CLI help/deprecation output now renders OSC 8 terminal hyperlinks
Expand Down
88 changes: 58 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,31 @@ When any spec has `inject: true`, the plugin strips existing
from the HTML before injecting the new set,
to prevent duplicates. `apple-touch-icon` tags are preserved.

### Embedding as `data:` URIs

`inject: 'embed'` inlines the favicon bytes directly into the `<link>` href as a
`data:` URI — the HTML carries the image itself, no file reference. Pair it with
`emit: false` to embed without writing a file at all.

```ts
svgToIco({
input: 'src/icon.svg',
emit: [
// ICO inlined as base64 AND written to disk (default emit: true).
{ format: 'ico', sizes: [16, 32], inject: 'embed' },
// SVG inlined as a utf8 data: URI, no file on disk.
{ format: 'svg', emit: false, inject: 'embed', encoding: 'utf8' },
],
});
```

Encoding (`SvgSpec` only): `base64` (default) is opaque and uniform; `utf8`
(`data:image/svg+xml,…`) keeps the markup readable and is usually smaller. The
SVG bytes are preserved verbatim — quotes and significant whitespace (including
CDATA and `xml:space="preserve"`) survive the round-trip unchanged. Binary ICO
and PNG are always base64. Embedded hrefs are never cache-busted, since the href
_is_ the content.

### Non-SVG input

PNG, JPEG, WebP, AVIF, GIF, and TIFF sources are supported — the plugin detects
Expand Down Expand Up @@ -133,13 +158,12 @@ svg-to-ico generate https://example.com/icon.svg --out-dir build
npx -y --package=vite-svg-to-ico svg-to-ico generate https://example.com/icon.svg --out-dir build
```

### Legacy v2 `emit` shape
### Migrating from the v2 `emit` shape

The `{ source, sizes, inject }` object shape from v2 still works
via a compatibility shim and logs a one-time deprecation warning.
It will be **removed in v4**. Migrate examples:
The `{ source, sizes, inject }` object shape was **removed in v4** — `emit`
now accepts only an `EmitSpec[]` array. Convert as follows:

| v2 | v3 |
| v2 (removed) | v3/v4 |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `emit: { source: true }` | `emit: [{ format: 'ico' }, { format: 'svg' }]` |
| `emit: { sizes: 'png' }` | `emit: [{ format: 'ico' }, { format: 'png', sizes: [16, 32, 48] }]` |
Expand Down Expand Up @@ -219,44 +243,48 @@ svgToIco({ input: 'src/icon.svg', dev: { hmr: false } });

## Options

| Option | Type | Default | Description |
| -------- | --------------------------------- | --------------------- | --------------------------------------------------------------------------------- |
| `input` | `string \| URL` | **(required)** | Source image: path, `URL` instance, or `file://` / `http(s)://` URL string. |
| `sizes` | `number \| number[]` | `[16, 32, 48]` | Default sizes used when an `IcoSpec` omits its own `sizes`. |
| `emit` | `EmitSpec[] \| LegacyEmitOptions` | `[{ format: 'ico' }]` | What to emit and inject. Array of specs (v3) or legacy object shape (deprecated). |
| `output` | `string` | `'favicon.ico'` | _Deprecated_. Fallback ICO filename when an `IcoSpec` omits `filename`. |
| `sharp` | `SharpOptions` | `{}` | Sharp image processing options. |
| `dev` | `boolean \| DevOptions` | `true` | Control dev-server behavior. |
| Option | Type | Default | Description |
| -------- | ----------------------- | --------------------- | --------------------------------------------------------------------------- |
| `input` | `string \| URL` | **(required)** | Source image: path, `URL` instance, or `file://` / `http(s)://` URL string. |
| `sizes` | `number \| number[]` | `[16, 32, 48]` | Default sizes used when an `IcoSpec` omits its own `sizes`. |
| `emit` | `EmitSpec[]` | `[{ format: 'ico' }]` | What to emit and inject — an array of per-format specs. |
| `output` | `string` | `'favicon.ico'` | Fallback ICO filename when an `IcoSpec` omits `filename`. |
| `sharp` | `SharpOptions` | `{}` | Sharp image processing options. |
| `dev` | `boolean \| DevOptions` | `true` | Control dev-server behavior. |

### `emit` (v3 — recommended)

Array of per-format specs. Each entry is one of:

#### `IcoSpec`

| Field | Type | Default | Description |
| ---------- | ----------- | ----------------- | ----------------------------------------------- |
| `format` | `'ico'` | — | Discriminator. |
| `sizes` | `number[]?` | Top-level `sizes` | Sizes to pack into this ICO (1–256). |
| `filename` | `string?` | `'favicon.ico'` | Output filename (relative to build output). |
| `inject` | `boolean?` | `false` | Inject `<link rel="icon" type="image/x-icon">`. |
| Field | Type | Default | Description |
| ---------- | -------------------- | ----------------- | ------------------------------------------------------------------------ |
| `format` | `'ico'` | — | Discriminator. |
| `sizes` | `number[]?` | Top-level `sizes` | Sizes to pack into this ICO (1–256). |
| `filename` | `string?` | `'favicon.ico'` | Output filename (relative to build output). |
| `emit` | `boolean?` | `true` | Write the ICO file. Set `false` to embed without writing (see `inject`). |
| `inject` | `boolean \| 'embed'` | `false` | `true` links the file; `'embed'` inlines the bytes as a `data:` URI. |

#### `PngSpec`

| Field | Type | Default | Description |
| ------------------ | ---------------------------------- | ----------------------------- | --------------------------------------------------------------------- |
| `format` | `'png'` | — | Discriminator. |
| `sizes` | `number[]` | **(required)** | Sizes to emit as separate PNG files (1–256). |
| `filenameTemplate` | `string?` | `'favicon-{size}x{size}.png'` | Template using `{size}` placeholder. |
| `inject` | `boolean \| { sizes?: number[] }?` | `false` | `true` injects all sizes; `{ sizes }` injects only the listed subset. |
| Field | Type | Default | Description |
| ------------------ | ------------------------------------------ | ----------------------------- | ---------------------------------------------------------------------------------- |
| `format` | `'png'` | — | Discriminator. |
| `sizes` | `number[]` | **(required)** | Sizes to emit as separate PNG files (1–4096 — not bound by ICO's 256 cap). |
| `filenameTemplate` | `string?` | `'favicon-{size}x{size}.png'` | Template using `{size}` placeholder. |
| `emit` | `boolean?` | `true` | Write the PNG files. Set `false` to embed without writing. |
| `inject` | `boolean \| 'embed' \| { sizes?, embed? }` | `false` | `true` links all sizes; `'embed'` inlines all; `{ sizes }` / `{ embed }` scope it. |

#### `SvgSpec`

| Field | Type | Default | Description |
| ---------- | ---------- | ----------------- | ------------------------------------------------------------ |
| `format` | `'svg'` | — | Discriminator. |
| `filename` | `string?` | `basename(input)` | Output filename (only meaningful when input is an SVG). |
| `inject` | `boolean?` | `false` | Inject `<link rel="icon" type="image/svg+xml" sizes="any">`. |
| Field | Type | Default | Description |
| ---------- | -------------------- | ----------------- | ----------------------------------------------------------------------------------- |
| `format` | `'svg'` | — | Discriminator. |
| `filename` | `string?` | `basename(input)` | Output filename (only meaningful when input is an SVG). |
| `emit` | `boolean?` | `true` | Write the SVG copy. Set `false` to embed without writing. |
| `inject` | `boolean \| 'embed'` | `false` | `true` links the file; `'embed'` inlines the SVG as a `data:` URI. |
| `encoding` | `'base64' \| 'utf8'` | `'base64'` | Embed encoding (only with `inject: 'embed'`). `utf8` is readable + usually smaller. |

### `emit` (v2 — deprecated, removed in v4)

Expand Down
8 changes: 0 additions & 8 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
[test]
preload = ["./preload.ts"]
coverage = true
coverageSkipTestFiles = true
coveragePathIgnorePatterns = ["tests/**"]
21 changes: 13 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
{
"name": "vite-svg-to-ico",
"version": "3.1.6",
"version": "4.0.0",
"description": "Vite plugin that converts SVG to ICO during site build.",
"workspaces": [
"tests/smoke/fixture"
],
"keywords": [
"vite-plugin",
"vite",
Expand Down Expand Up @@ -32,13 +29,19 @@
"#vite-svg-to-ico": "./src/index.ts",
"#internals/*": "./src/*",
"#types": "./src/types.ts",
"#html": "./src/html.ts",
"#faviconTags": "./src/favicon-tags.ts",
"#injectHtml": "./src/inject-html.ts",
"#ico": "./src/ico.ts",
"#raster": "./src/raster.ts",
"#size": "./src/size.ts",
"#cli/*": "./src/cli/*.ts",
"#cli": "./src/cli.ts",
"#assets": "./src/assets.ts",
"#config": "./src/config.ts",
"#devClient": "./src/dev-client.ts",
"#instrumentation": "./src/instrumentation.ts",
"#normalizeEmit": "./src/normalize-emit.ts",
"#loadInput": "./src/load-input.ts",
"#dataUri": "./src/data-uri.ts",
"#resolveSpecs": "./src/resolve-specs.ts",
"#pkg": "./package.json"
},
Expand All @@ -50,11 +53,12 @@
"build": "tsdown",
"dev": "tsdown --watch",
"fmt": "dprint fmt",
"fmt:cfg:up": "dprint config update -y",
"fmt:cfg:up": "dprint config update -yr",
"fmt:autofix": "dprint fmt --allow-no-files --diff --excludes .github",
"prepack": "bun --bun bd -l error",
"prepublishOnly": "run -s test typecheck",
"tar": "bun pm pack --quiet | awk 'NF{line=$0} END{print line}'",
"pretest": "bun pm pack --gzip-level 0 --filename tests/smoke/fixture/vite-svg-to-ico.tgz && bun --cwd=tests/smoke/fixture install --no-frozen-lockfile",
"test": "bun test",
"typecheck": "tsgo --noEmit",
"lint": "biome lint"
Expand All @@ -80,7 +84,8 @@
"fflate": "0.8.2"
},
"engines": {
"node": ">=22.18.0"
"node": ">=22.22.2",
"bun": ">=1.3"
},
"packageManager": "bun@1.3.14",
"publishConfig": {
Expand Down
Loading