From 90b471c1231b81f19e4dd0a14cfbb5f1721dbad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Sun, 22 Mar 2020 09:23:27 +0100 Subject: [PATCH] fix(svgo): support any SVGO config format Fixes #400 --- .../src/__snapshots__/index.test.js.snap | 20 +-- packages/plugin-svgo/src/config.js | 48 +++++++ packages/plugin-svgo/src/config.test.js | 133 ++++++++++++++++++ packages/plugin-svgo/src/index.js | 47 +------ packages/plugin-svgo/src/index.test.js | 35 +++-- 5 files changed, 215 insertions(+), 68 deletions(-) create mode 100644 packages/plugin-svgo/src/config.js create mode 100644 packages/plugin-svgo/src/config.test.js diff --git a/packages/plugin-svgo/src/__snapshots__/index.test.js.snap b/packages/plugin-svgo/src/__snapshots__/index.test.js.snap index ef9cec5e..f6d891db 100644 --- a/packages/plugin-svgo/src/__snapshots__/index.test.js.snap +++ b/packages/plugin-svgo/src/__snapshots__/index.test.js.snap @@ -1,19 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`svgo should be possible to disable id prefixing 1`] = `""`; +exports[`svgo disables id prefixing using svgo config 1`] = `""`; -exports[`svgo should not load runtime configuration with \`runtimeConfig: false\` 1`] = `""`; +exports[`svgo does not load runtime configuration with \`runtimeConfig: false\` 1`] = `""`; -exports[`svgo should not remove viewBox with icon option 1`] = `""`; +exports[`svgo does not remove viewBox with \`icon\` option 1`] = `""`; -exports[`svgo should not remove viewBox with when dimensions is false 1`] = `""`; +exports[`svgo does not remove viewBox with when \`dimensions\` is false 1`] = `""`; -exports[`svgo should optimize svg 1`] = `""`; +exports[`svgo is possible to enable id prefixing as the only optimization 1`] = `"DismissCreated with Sketch."`; -exports[`svgo should support config.svgoConfig 1`] = `"Created with Sketch."`; +exports[`svgo optimizes svg 1`] = `""`; -exports[`svgo should support icon with config.svgoConfig plugins 1`] = `"Created with Sketch."`; +exports[`svgo supports \`config.icon\` with \`config.svgoConfig\` plugins 1`] = `"Created with Sketch."`; -exports[`svgo should use state.filePath to detect configuration 1`] = `""`; +exports[`svgo supports \`config.svgoConfig.multipass\` 1`] = `""`; -exports[`svgo should be possible to enable id prefixing as the only optimization 1`] = `"DismissCreated with Sketch."`; +exports[`svgo supports \`config.svgoConfig\` 1`] = `"Created with Sketch."`; + +exports[`svgo users \`state.filePath\` to detect configuration 1`] = `""`; diff --git a/packages/plugin-svgo/src/config.js b/packages/plugin-svgo/src/config.js new file mode 100644 index 00000000..0de6c9f0 --- /dev/null +++ b/packages/plugin-svgo/src/config.js @@ -0,0 +1,48 @@ +import mergeDeep from 'merge-deep' + +export function getFilePath(state) { + return state.filePath || process.cwd() +} + +export function getBaseSvgoConfig(config) { + const baseSvgoConfig = { + plugins: [{ prefixIds: true }], + } + if (config.icon || config.dimensions === false) { + baseSvgoConfig.plugins.push({ removeViewBox: false }) + } + return baseSvgoConfig +} + +export function getPlugins(config) { + if (!config || !config.plugins) { + return [] + } + if (!Array.isArray(config.plugins)) { + throw Error('`svgoConfig.plugins` must be an array') + } + return config.plugins +} + +function extractPlugins(config) { + if (!config) return [] + if (!config.plugins) return [] + if (!Array.isArray(config.plugins)) return [config.plugins] + return config.plugins +} + +function mergePlugins(configs) { + const plugins = configs.reduce( + (merged, config) => mergeDeep(merged, ...extractPlugins(config)), + {}, + ) + return Object.keys(plugins).reduce((array, key) => { + array.push({ [key]: plugins[key] }) + return array + }, []) +} + +export function mergeSvgoConfig(...configs) { + const plugins = mergePlugins(configs) + return { ...mergeDeep(...configs), plugins } +} diff --git a/packages/plugin-svgo/src/config.test.js b/packages/plugin-svgo/src/config.test.js new file mode 100644 index 00000000..bb2946fc --- /dev/null +++ b/packages/plugin-svgo/src/config.test.js @@ -0,0 +1,133 @@ +import { getFilePath, getBaseSvgoConfig, mergeSvgoConfig } from './config' + +describe('svgo config', () => { + describe('#getFilePath', () => { + describe('if `state.filePath` exists', () => { + it('returns `state.filePath', () => { + expect(getFilePath({ filePath: '/foo/bar' })).toBe('/foo/bar') + }) + }) + describe('if `state.filePath` does not exists', () => { + it('returns current working directory', () => { + expect(getFilePath({})).toBe(process.cwd()) + }) + }) + }) + + describe('#getBaseSvgoConfig', () => { + describe('with no specific config', () => { + it('returns config with `prefixIds: true`', () => { + expect(getBaseSvgoConfig({})).toEqual({ + plugins: [{ prefixIds: true }], + }) + }) + }) + + describe('with `config.icons` enabled', () => { + it('returns config with `removeViewBox: false`', () => { + expect(getBaseSvgoConfig({ icon: true })).toEqual({ + plugins: [{ prefixIds: true }, { removeViewBox: false }], + }) + }) + }) + + describe('with `config.dimensions` disabled', () => { + it('returns config with `removeViewBox: false`', () => { + expect(getBaseSvgoConfig({ dimensions: false })).toEqual({ + plugins: [{ prefixIds: true }, { removeViewBox: false }], + }) + }) + }) + }) + + describe('#mergeSvgoConfig', () => { + it('merges any config format', () => { + expect(mergeSvgoConfig({ foo: 'bar' }, { foo: 'rab' })).toEqual({ + foo: 'rab', + plugins: [], + }) + expect( + mergeSvgoConfig({ plugins: { removeViewBox: false } }, null), + ).toEqual({ + plugins: [{ removeViewBox: false }], + }) + expect( + mergeSvgoConfig({ plugins: { removeViewBox: false } }, {}), + ).toEqual({ + plugins: [{ removeViewBox: false }], + }) + expect(mergeSvgoConfig({ plugins: { removeViewBox: false } })).toEqual({ + plugins: [{ removeViewBox: false }], + }) + expect(mergeSvgoConfig({ plugins: [{ removeViewBox: false }] })).toEqual({ + plugins: [{ removeViewBox: false }], + }) + expect( + mergeSvgoConfig({ + plugins: [{ removeViewBox: false }, { removeViewBox: true }], + }), + ).toEqual({ + plugins: [{ removeViewBox: true }], + }) + expect( + mergeSvgoConfig({ + plugins: [ + { + convertColors: { + currentColor: true, + }, + }, + { + prefixIds: { + prefix: 'foo', + }, + }, + ], + }), + ).toEqual({ + plugins: [ + { + convertColors: { + currentColor: true, + }, + }, + { + prefixIds: { + prefix: 'foo', + }, + }, + ], + }) + expect( + mergeSvgoConfig( + { + plugins: [ + { + prefixIds: { + prefix: 'foo', + }, + }, + ], + }, + { + plugins: [ + { + prefixIds: { + prefix: 'bar', + }, + }, + ], + }, + ), + ).toEqual({ + plugins: [ + { + prefixIds: { + prefix: 'bar', + }, + }, + ], + }) + }) + }) +}) diff --git a/packages/plugin-svgo/src/index.js b/packages/plugin-svgo/src/index.js index 5c713b40..4fb9908e 100644 --- a/packages/plugin-svgo/src/index.js +++ b/packages/plugin-svgo/src/index.js @@ -1,7 +1,7 @@ /* eslint-disable no-underscore-dangle */ import SVGO from 'svgo' import { cosmiconfigSync } from 'cosmiconfig' -import mergeDeep from 'merge-deep' +import { getFilePath, getBaseSvgoConfig, mergeSvgoConfig } from './config' const explorer = cosmiconfigSync('svgo', { searchPlaces: [ @@ -83,54 +83,13 @@ function optimizeSync(svgstr, info) { return result } -function getBaseSvgoConfig(config) { - const baseSvgoConfig = { - plugins: [{ prefixIds: true }], - } - if (config.icon || config.dimensions === false) - baseSvgoConfig.plugins.push({ removeViewBox: false }) - return baseSvgoConfig -} - -function getFilePath(state) { - return state.filePath || process.cwd() -} - -function getPlugins(config) { - if (!config || !config.plugins) { - return [] - } - if (!Array.isArray(config.plugins)) { - throw Error("`svgoConfig.plugins` must be an array") - } - return config.plugins -} - -function extendPlugins(...configs) { - const init = []; - let i = configs.length; - - while (i-- > 0) { - const plugins = configs[i]; - for (let j = 0; j < plugins.length; j++) { - const plugin = plugins[j]; - if (!init.some(item => Object.keys(item)[0] === Object.keys(plugin)[0])) { - init.push(plugin); - } - } - } - return init; -} - function createSvgo(config, rcConfig) { - const baseSvgoConfig = getBaseSvgoConfig(config); - const plugins = extendPlugins(getPlugins(baseSvgoConfig), getPlugins(rcConfig), getPlugins(config.svgoConfig)); - const mergedConfig = mergeDeep( + const baseSvgoConfig = getBaseSvgoConfig(config) + const mergedConfig = mergeSvgoConfig( baseSvgoConfig, rcConfig, config.svgoConfig, ) - mergedConfig.plugins = plugins return new SVGO(mergedConfig) } diff --git a/packages/plugin-svgo/src/index.test.js b/packages/plugin-svgo/src/index.test.js index 3050fdd1..9491baf9 100644 --- a/packages/plugin-svgo/src/index.test.js +++ b/packages/plugin-svgo/src/index.test.js @@ -19,12 +19,12 @@ const baseSvg = ` ` describe('svgo', () => { - it('should optimize svg', () => { + it('optimizes svg', () => { const result = svgo(baseSvg, { svgo: true, runtimeConfig: true }, {}) expect(result).toMatchSnapshot() }) - it('should support config.svgoConfig', () => { + it('supports `config.svgoConfig`', () => { const result = svgo( baseSvg, { @@ -38,21 +38,21 @@ describe('svgo', () => { expect(result).toMatchSnapshot() }) - it('should throw error for invalid config.svgoConfig', () => { - const svgoOptions = [ + it('supports `config.svgoConfig.multipass`', () => { + const result = svgo( baseSvg, { svgo: true, runtimeConfig: true, - svgoConfig: { plugins: { removeDesc: false } }, + svgoConfig: { multipass: true }, }, {}, - ] + ) - expect(() => svgo(...svgoOptions)).toThrow() + expect(result).toMatchSnapshot() }) - it('should support icon with config.svgoConfig plugins', () => { + it('supports `config.icon` with `config.svgoConfig` plugins', () => { const result = svgo( baseSvg, { @@ -67,7 +67,7 @@ describe('svgo', () => { expect(result).toMatchSnapshot() }) - it('should use state.filePath to detect configuration', () => { + it('users `state.filePath` to detect configuration', () => { const result = svgo( baseSvg, { svgo: true, runtimeConfig: true }, @@ -77,7 +77,7 @@ describe('svgo', () => { expect(result).toMatchSnapshot() }) - it('should not load runtime configuration with `runtimeConfig: false`', () => { + it('does not load runtime configuration with `runtimeConfig: false`', () => { const result = svgo( baseSvg, { svgo: true, runtimeConfig: false }, @@ -87,7 +87,7 @@ describe('svgo', () => { expect(result).toMatchSnapshot() }) - it('should not remove viewBox with icon option', () => { + it('does not remove viewBox with `icon` option', () => { const result = svgo( baseSvg, { svgo: true, icon: true, runtimeConfig: true }, @@ -97,7 +97,7 @@ describe('svgo', () => { expect(result).toMatchSnapshot() }) - it('should not remove viewBox with when dimensions is false', () => { + it('does not remove viewBox with when `dimensions` is false', () => { const result = svgo( baseSvg, { svgo: true, dimensions: false, runtimeConfig: true }, @@ -107,7 +107,7 @@ describe('svgo', () => { expect(result).toMatchSnapshot() }) - it('should be possible to disable id prefixing', () => { + it('disables id prefixing using svgo config', () => { const result = svgo( baseSvg, { @@ -122,14 +122,19 @@ describe('svgo', () => { expect(result).toMatchSnapshot() }) - it('should be possible to enable id prefixing as the only optimization', () => { + it('is possible to enable id prefixing as the only optimization', () => { const result = svgo( baseSvg, { svgo: true, icon: true, runtimeConfig: true, - svgoConfig: { full: true, plugins: [{ prefixIds: {prefixIds: true, prefixClassNames: false} }] }, + svgoConfig: { + full: true, + plugins: [ + { prefixIds: { prefixIds: true, prefixClassNames: false } }, + ], + }, }, { filePath: path.join(__dirname, '../__fixtures__/svgo') }, )