From 36297f212983972f18c014d8c28f78b3902ef969 Mon Sep 17 00:00:00 2001 From: neverland Date: Mon, 22 Jul 2024 15:01:19 +0800 Subject: [PATCH] fix(plugin-svgr): dedupe SVGO plugins config (#2984) --- .../index.test.ts | 2 +- .../svg/svgr-exclude-importer/index.test.ts | 2 +- e2e/cases/svg/svgr-exclude/index.test.ts | 2 +- .../svg/svgr-external-react/index.test.ts | 2 +- .../svgr-named-export-component/index.test.ts | 2 +- .../svgr-override-svgo-options/index.test.ts | 17 +++ .../rsbuild.config.ts | 25 ++++ .../svgr-override-svgo-options/src/App.jsx | 12 ++ .../svgr-override-svgo-options/src/index.js | 9 ++ .../src/with-id.svg | 2 + packages/plugin-svgr/package.json | 1 + packages/plugin-svgr/src/index.ts | 108 +++++++++++++++--- pnpm-lock.yaml | 3 + website/docs/en/plugins/list/plugin-svgr.mdx | 40 +++++++ website/docs/zh/plugins/list/plugin-svgr.mdx | 40 +++++++ 15 files changed, 243 insertions(+), 24 deletions(-) create mode 100644 e2e/cases/svg/svgr-override-svgo-options/index.test.ts create mode 100644 e2e/cases/svg/svgr-override-svgo-options/rsbuild.config.ts create mode 100644 e2e/cases/svg/svgr-override-svgo-options/src/App.jsx create mode 100644 e2e/cases/svg/svgr-override-svgo-options/src/index.js create mode 100644 e2e/cases/svg/svgr-override-svgo-options/src/with-id.svg diff --git a/e2e/cases/svg/svgr-default-export-component/index.test.ts b/e2e/cases/svg/svgr-default-export-component/index.test.ts index ae95f289eb..43a1b33f68 100644 --- a/e2e/cases/svg/svgr-default-export-component/index.test.ts +++ b/e2e/cases/svg/svgr-default-export-component/index.test.ts @@ -1,7 +1,7 @@ import { build, gotoPage } from '@e2e/helper'; import { expect, test } from '@playwright/test'; -test('Use SVGR and default export React component', async ({ page }) => { +test('use SVGR and default export React component', async ({ page }) => { const rsbuild = await build({ cwd: __dirname, runServer: true, diff --git a/e2e/cases/svg/svgr-exclude-importer/index.test.ts b/e2e/cases/svg/svgr-exclude-importer/index.test.ts index d1404ea49c..91df92fd62 100644 --- a/e2e/cases/svg/svgr-exclude-importer/index.test.ts +++ b/e2e/cases/svg/svgr-exclude-importer/index.test.ts @@ -1,7 +1,7 @@ import { build, gotoPage } from '@e2e/helper'; import { expect, test } from '@playwright/test'; -test('Use SVGR and exclude some files', async ({ page }) => { +test('use SVGR and exclude some files', async ({ page }) => { const rsbuild = await build({ cwd: __dirname, runServer: true, diff --git a/e2e/cases/svg/svgr-exclude/index.test.ts b/e2e/cases/svg/svgr-exclude/index.test.ts index d1404ea49c..91df92fd62 100644 --- a/e2e/cases/svg/svgr-exclude/index.test.ts +++ b/e2e/cases/svg/svgr-exclude/index.test.ts @@ -1,7 +1,7 @@ import { build, gotoPage } from '@e2e/helper'; import { expect, test } from '@playwright/test'; -test('Use SVGR and exclude some files', async ({ page }) => { +test('use SVGR and exclude some files', async ({ page }) => { const rsbuild = await build({ cwd: __dirname, runServer: true, diff --git a/e2e/cases/svg/svgr-external-react/index.test.ts b/e2e/cases/svg/svgr-external-react/index.test.ts index cf388dfffa..92998b752f 100644 --- a/e2e/cases/svg/svgr-external-react/index.test.ts +++ b/e2e/cases/svg/svgr-external-react/index.test.ts @@ -2,7 +2,7 @@ import { build, gotoPage } from '@e2e/helper'; import { expect, test } from '@playwright/test'; // It's an old bug when use svgr in css and external react. -test('Use SVGR and externals react', async ({ page }) => { +test('use SVGR and externals react', async ({ page }) => { const rsbuild = await build({ cwd: __dirname, runServer: true, diff --git a/e2e/cases/svg/svgr-named-export-component/index.test.ts b/e2e/cases/svg/svgr-named-export-component/index.test.ts index ae95f289eb..43a1b33f68 100644 --- a/e2e/cases/svg/svgr-named-export-component/index.test.ts +++ b/e2e/cases/svg/svgr-named-export-component/index.test.ts @@ -1,7 +1,7 @@ import { build, gotoPage } from '@e2e/helper'; import { expect, test } from '@playwright/test'; -test('Use SVGR and default export React component', async ({ page }) => { +test('use SVGR and default export React component', async ({ page }) => { const rsbuild = await build({ cwd: __dirname, runServer: true, diff --git a/e2e/cases/svg/svgr-override-svgo-options/index.test.ts b/e2e/cases/svg/svgr-override-svgo-options/index.test.ts new file mode 100644 index 0000000000..dd8d325201 --- /dev/null +++ b/e2e/cases/svg/svgr-override-svgo-options/index.test.ts @@ -0,0 +1,17 @@ +import { build, gotoPage } from '@e2e/helper'; +import { expect, test } from '@playwright/test'; + +test('use SVGR and override svgo plugin options', async ({ page }) => { + const rsbuild = await build({ + cwd: __dirname, + runServer: true, + }); + + await gotoPage(page, rsbuild); + + await expect( + page.evaluate(`document.getElementById('test-svg').tagName === 'svg'`), + ).resolves.toBeTruthy(); + + await rsbuild.close(); +}); diff --git a/e2e/cases/svg/svgr-override-svgo-options/rsbuild.config.ts b/e2e/cases/svg/svgr-override-svgo-options/rsbuild.config.ts new file mode 100644 index 0000000000..aa635d7d1b --- /dev/null +++ b/e2e/cases/svg/svgr-override-svgo-options/rsbuild.config.ts @@ -0,0 +1,25 @@ +import { pluginReact } from '@rsbuild/plugin-react'; +import { pluginSvgr } from '@rsbuild/plugin-svgr'; + +export default { + plugins: [ + pluginReact(), + pluginSvgr({ + svgrOptions: { + svgoConfig: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + cleanupIds: false, + }, + }, + }, + ], + }, + }, + }), + ], +}; diff --git a/e2e/cases/svg/svgr-override-svgo-options/src/App.jsx b/e2e/cases/svg/svgr-override-svgo-options/src/App.jsx new file mode 100644 index 0000000000..6695c69db3 --- /dev/null +++ b/e2e/cases/svg/svgr-override-svgo-options/src/App.jsx @@ -0,0 +1,12 @@ +import Logo from './with-id.svg?react'; + +function App() { + return ( +
+
Hello Rsbuild!
+ +
+ ); +} + +export default App; diff --git a/e2e/cases/svg/svgr-override-svgo-options/src/index.js b/e2e/cases/svg/svgr-override-svgo-options/src/index.js new file mode 100644 index 0000000000..5ceb026ccc --- /dev/null +++ b/e2e/cases/svg/svgr-override-svgo-options/src/index.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render(React.createElement(App)); +} diff --git a/e2e/cases/svg/svgr-override-svgo-options/src/with-id.svg b/e2e/cases/svg/svgr-override-svgo-options/src/with-id.svg new file mode 100644 index 0000000000..b828da82dd --- /dev/null +++ b/e2e/cases/svg/svgr-override-svgo-options/src/with-id.svg @@ -0,0 +1,2 @@ + + diff --git a/packages/plugin-svgr/package.json b/packages/plugin-svgr/package.json index 9938028fe1..848bda8547 100644 --- a/packages/plugin-svgr/package.json +++ b/packages/plugin-svgr/package.json @@ -41,6 +41,7 @@ "@types/node": "18.x", "file-loader": "6.2.0", "prebundle": "1.2.2", + "svgo": "^3.3.2", "typescript": "^5.5.2", "url-loader": "4.1.1" }, diff --git a/packages/plugin-svgr/src/index.ts b/packages/plugin-svgr/src/index.ts index 71e093cb71..2dc0ce2324 100644 --- a/packages/plugin-svgr/src/index.ts +++ b/packages/plugin-svgr/src/index.ts @@ -1,8 +1,11 @@ import path from 'node:path'; import type { RsbuildPlugin, Rspack } from '@rsbuild/core'; import { PLUGIN_REACT_NAME } from '@rsbuild/plugin-react'; -import type { Config } from '@svgr/core'; +import type { Config as SvgrOptions } from '@svgr/core'; import deepmerge from 'deepmerge'; +import type { Config as SvgoConfig } from 'svgo'; + +type SvgoPluginConfig = NonNullable[0]; export type SvgDefaultExport = 'component' | 'url'; @@ -13,7 +16,7 @@ export type PluginSvgrOptions = { * Configure SVGR options. * @see https://react-svgr.com/docs/options/ */ - svgrOptions?: Config; + svgrOptions?: SvgrOptions; /** * Whether to allow the use of default import and named import at the same time. @@ -38,23 +41,88 @@ export type PluginSvgrOptions = { excludeImporter?: Rspack.RuleSetCondition; }; -function getSvgoDefaultConfig() { - return { - plugins: [ - { - name: 'preset-default', - params: { - overrides: { - // viewBox is required to resize SVGs with CSS. - // @see https://github.com/svg/svgo/issues/1128 - removeViewBox: false, - }, +const getSvgoDefaultConfig = (): SvgoConfig => ({ + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + // viewBox is required to resize SVGs with CSS. + // @see https://github.com/svg/svgo/issues/1128 + removeViewBox: false, }, }, - 'prefixIds', - ], - }; -} + }, + 'prefixIds', + ], +}); + +/** + * Dedupe SVGO plugins config. + * + * @example + * Input: + * { + * plugins: [ + * { name: 'preset-default', params: { foo: true }], + * { name: 'preset-default', params: { bar: true }], + * ] + * } + * Output: + * { + * plugins: [ + * { name: 'preset-default', params: { foo: true, bar: true }], + * ] + * } + */ +const dedupeSvgoPlugins = (config: SvgoConfig): SvgoConfig => { + if (!config.plugins) { + return config; + } + + let mergedPlugins: SvgoPluginConfig[] = []; + + for (const plugin of config.plugins) { + if (typeof plugin === 'string') { + const exist = mergedPlugins.find( + (item) => + item === plugin || (typeof item === 'object' && item.name === plugin), + ); + + if (!exist) { + mergedPlugins.push(plugin); + } + + continue; + } + + const strIndex = mergedPlugins.findIndex( + (item) => typeof item === 'string' && item === plugin.name, + ); + if (strIndex !== -1) { + mergedPlugins[strIndex] = plugin; + continue; + } + + let isMerged = false; + + mergedPlugins = mergedPlugins.map((item) => { + if (typeof item === 'object' && item.name === plugin.name) { + isMerged = true; + return deepmerge(item, plugin); + } + return item; + }); + + if (!isMerged) { + mergedPlugins.push(plugin); + } + } + + config.plugins = mergedPlugins; + + return config; +}; export const PLUGIN_SVGR_NAME = 'rsbuild:svgr'; @@ -85,7 +153,7 @@ export const pluginSvgr = (options: PluginSvgrOptions = {}): RsbuildPlugin => ({ const rule = chain.module.rule(CHAIN_ID.RULE.SVG).test(SVG_REGEX); - const svgrOptions = deepmerge( + const svgrOptions = deepmerge( { svgo: true, svgoConfig: getSvgoDefaultConfig(), @@ -93,6 +161,8 @@ export const pluginSvgr = (options: PluginSvgrOptions = {}): RsbuildPlugin => ({ options.svgrOptions || {}, ); + svgrOptions.svgoConfig = dedupeSvgoPlugins(svgrOptions.svgoConfig); + // force to url: "foo.svg?url", rule .oneOf(CHAIN_ID.ONE_OF.SVG_URL) @@ -116,7 +186,7 @@ export const pluginSvgr = (options: PluginSvgrOptions = {}): RsbuildPlugin => ({ .options({ ...svgrOptions, exportType: 'default', - } satisfies Config) + } satisfies SvgrOptions) .end(); // SVG in JS files diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce1055f0dd..7541b727b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1203,6 +1203,9 @@ importers: prebundle: specifier: 1.2.2 version: 1.2.2(typescript@5.5.2) + svgo: + specifier: ^3.3.2 + version: 3.3.2 typescript: specifier: ^5.5.2 version: 5.5.2 diff --git a/website/docs/en/plugins/list/plugin-svgr.mdx b/website/docs/en/plugins/list/plugin-svgr.mdx index 301484d037..5c2c4afe8d 100644 --- a/website/docs/en/plugins/list/plugin-svgr.mdx +++ b/website/docs/en/plugins/list/plugin-svgr.mdx @@ -118,6 +118,46 @@ pluginSvgr({ }); ``` +When you set `svgoConfig.plugins`, the configuration for plugins with the same name is automatically merged. For example, the following configuration will be merged with the built-in `preset-default`: + +```ts +pluginSvgr({ + svgrOptions: { + svgoConfig: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + cleanupIds: false, + }, + }, + }, + ], + }, + }, +}); +``` + +The merged `svgoConfig` will be: + +```ts +const mergedSvgoConfig = { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: true, + cleanupIds: false, + }, + }, + }, + 'prefixIds', + ], +}; +``` + ### svgrOptions.exportType Set the export type of SVG React components. diff --git a/website/docs/zh/plugins/list/plugin-svgr.mdx b/website/docs/zh/plugins/list/plugin-svgr.mdx index 9a2cb54451..bbc3d8e86e 100644 --- a/website/docs/zh/plugins/list/plugin-svgr.mdx +++ b/website/docs/zh/plugins/list/plugin-svgr.mdx @@ -118,6 +118,46 @@ pluginSvgr({ }); ``` +当你设置 `svgoConfig.plugins` 时,同名 plugin 的配置会被自动合并,比如下面的配置会与内置的 `preset-default` 进行合并: + +```ts +pluginSvgr({ + svgrOptions: { + svgoConfig: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + cleanupIds: false, + }, + }, + }, + ], + }, + }, +}); +``` + +合并后的 `svgoConfig` `如下: + +```ts +const mergedSvgoConfig = { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: true, + cleanupIds: false, + }, + }, + }, + 'prefixIds', + ], +}; +``` + ### svgrOptions.exportType 设置 SVG React 组件的导出方式。