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 (
+
+ );
+}
+
+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 组件的导出方式。