From 6850ae104f437da22d69936c1abb1a9117b0e033 Mon Sep 17 00:00:00 2001 From: Liam Potter Date: Mon, 26 Oct 2026 12:07:20 +0000 Subject: [PATCH] Export the baseline SVGO config and fix the svgo option docs A custom svgo config replaces the baseline config, but the README and the SvgJarOptions comment said it was deep-merged. Export baselineSvgoConfig so users can extend the baseline by copying its plugins, and correct the docs. The export is frozen so one caller cannot change the default for every build. Co-Authored-By: Claude Opus 5.5 --- plugin/README.md | 21 ++++++++++++++--- plugin/src/core/options.ts | 5 ++-- plugin/src/index.ts | 1 + plugin/src/svg/optimize.ts | 25 ++++++++++++++++---- plugin/test/unit/optimize.test.ts | 38 +++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/plugin/README.md b/plugin/README.md index 7375ed0..2c25772 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -126,10 +126,10 @@ svgJar({ // Default: 'dom' target: 'dom' | 'ember' | 'react' | 'preact' | 'vue' | 'solid' | 'web-component', - // SVGO configuration. - // true (default): use baseline config with sensible defaults + // SVGO configuration. See "SVGO optimization" below. + // true (default): use the baseline config // false: disable optimization entirely - // object: custom SVGO config, deep-merged with baseline + // object: custom SVGO config, used in place of the baseline svgo: true, // Default sprite name for bare SVG imports. @@ -417,6 +417,21 @@ SVGO runs per-SVG during the `load` phase (not on assembled sprites) to prevent Runs in both dev and prod so behaviour is consistent. +A custom `svgo` config replaces the baseline config. svg-jar does not merge them. To keep the baseline and add plugins, copy the baseline plugins from `baselineSvgoConfig`: + +```ts +import { baselineSvgoConfig } from '@svg-jar/plugin'; +import svgJar from '@svg-jar/plugin/vite'; + +svgJar({ + svgo: { + plugins: [...baselineSvgoConfig.plugins, 'removeDimensions'], + }, +}); +``` + +If you write a config without the baseline, keep the `cleanupIds` override from it: `{ name: 'preset-default', params: { overrides: { cleanupIds: { minify: false, remove: false } } } }`. Without it, SVGO shortens IDs in each file to names like `a` and `b`. Icons from different files then use the same IDs in one sprite sheet, and a `url(#a)` reference can point to the gradient or clip path of a different icon. + ### Embedded reference resolution SVGs containing `` or `` have their references resolved through the bundler's module graph: diff --git a/plugin/src/core/options.ts b/plugin/src/core/options.ts index 43336e9..0752275 100644 --- a/plugin/src/core/options.ts +++ b/plugin/src/core/options.ts @@ -13,9 +13,10 @@ export interface SvgJarOptions { /** * SVGO configuration. - * - `true` (default): use baseline config with sensible defaults + * - `true` (default): use `baselineSvgoConfig` * - `false`: disable optimization entirely - * - object: custom SVGO config, deep-merged with baseline + * - object: custom SVGO config, used in place of the baseline. To extend + * the baseline, copy `baselineSvgoConfig.plugins` into it. */ svgo?: boolean | SvgoConfig; diff --git a/plugin/src/index.ts b/plugin/src/index.ts index 65fa940..5bf66ce 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -77,3 +77,4 @@ export const SvgJarPlugin: UnpluginInstance = }); export type { SvgJarOptions, SvgJarTarget } from './core/options.ts'; +export { baselineSvgoConfig } from './svg/optimize.ts'; diff --git a/plugin/src/svg/optimize.ts b/plugin/src/svg/optimize.ts index 4ebf9fc..abe31e7 100644 --- a/plugin/src/svg/optimize.ts +++ b/plugin/src/svg/optimize.ts @@ -1,15 +1,30 @@ -import { optimize, type Config as SvgoConfig } from 'svgo'; +import { optimize, type Config as SvgoConfig, type PluginConfig } from 'svgo'; export type { Config as SvgoConfig } from 'svgo'; +/** Freezes an object and every object inside it. */ +function deepFreeze(value: T): T { + for (const child of Object.values(value)) { + if (typeof child === 'object' && child !== null) deepFreeze(child); + } + return Object.freeze(value); +} + /** * Baseline SVGO configuration used when `svgo: true` (the default). * * Preserves IDs (needed for sprite symbol references), path data * precision, numeric values, and hidden elements. Strips `` * elements since they cause tooltip issues in browsers. + * + * A custom `svgo` config replaces this one. To extend it, copy its plugins + * into the custom config. It is frozen so that changes made by one caller + * cannot change the default for every build. + * + * @example + * svgJar({ svgo: { plugins: [...baselineSvgoConfig.plugins, 'removeDimensions'] } }) */ -const BASELINE_CONFIG: SvgoConfig = { +export const baselineSvgoConfig: { readonly plugins: readonly PluginConfig[] } = deepFreeze({ plugins: [ { name: 'preset-default', @@ -24,14 +39,14 @@ const BASELINE_CONFIG: SvgoConfig = { }, 'removeTitle', ], -}; +}); /** * Optimizes an SVG string using SVGO. * * @param svg The raw SVG string to optimize. * @param config Controls optimization behavior: - * - `true`: use baseline config (default) + * - `true`: use `baselineSvgoConfig` (default) * - `false`: return the SVG unchanged * - object: custom SVGO config (used as-is, not merged) * @returns The optimized SVG string. @@ -46,7 +61,7 @@ export function optimizeSvg(svg: string, config: boolean | SvgoConfig): string { return svg; } - const svgoConfig = config === true ? BASELINE_CONFIG : config; + const svgoConfig = config === true ? { plugins: [...baselineSvgoConfig.plugins] } : config; const result = optimize(svg, svgoConfig); return result.data; } diff --git a/plugin/test/unit/optimize.test.ts b/plugin/test/unit/optimize.test.ts index 53e23e7..c8701d6 100644 --- a/plugin/test/unit/optimize.test.ts +++ b/plugin/test/unit/optimize.test.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { describe, it, expect } from 'vitest'; import { optimizeSvg } from '../../src/svg/optimize.ts'; +import { baselineSvgoConfig } from '../../src/index.ts'; const FIXTURES_DIR = path.join(import.meta.dirname, '..', '_fixtures'); @@ -71,6 +72,34 @@ describe('optimizeSvg', () => { }); }); + describe('baselineSvgoConfig', () => { + it('is the config used when svgo is true', () => { + const input = readFixture('source-order.svg'); + + expect(optimizeSvg(input, { plugins: [...baselineSvgoConfig.plugins] })).toBe(optimizeSvg(input, true)); + }); + + it('can be extended with more plugins', () => { + const input = readFixture('with-dimensions.svg'); + const output = optimizeSvg(input, { plugins: [...baselineSvgoConfig.plugins, 'removeDimensions'] }); + + // removeDimensions ran + expect(output).not.toContain('width="24"'); + // The baseline cleanupIds settings still apply + expect(optimizeSvg(readFixture('nested-groups.svg'), { plugins: [...baselineSvgoConfig.plugins] })).toContain( + 'id="outer"', + ); + }); + + it('cannot be changed by the caller', () => { + expect(Object.isFrozen(baselineSvgoConfig)).toBe(true); + expect(Object.isFrozen(baselineSvgoConfig.plugins)).toBe(true); + expect(() => { + (baselineSvgoConfig.plugins as unknown[]).push('removeDimensions'); + }).toThrow(TypeError); + }); + }); + describe('with custom config', () => { it('applies the custom SVGO config', () => { const input = @@ -82,5 +111,14 @@ describe('optimizeSvg', () => { expect(output).not.toContain('<!-- comment -->'); expect(output).toContain('<path'); }); + + it('uses the custom config in place of the baseline, without merging', () => { + const input = + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>My Icon'; + const output = optimizeSvg(input, { plugins: ['removeComments'] }); + + // The baseline removes , so its presence shows the baseline did not run + expect(output).toContain('<title>My Icon'); + }); }); });