diff --git a/e2e/cases/svg/svgr-query-react/index.test.ts b/e2e/cases/svg/svgr-query-react/index.test.ts
new file mode 100644
index 0000000000..bf25b0dfd2
--- /dev/null
+++ b/e2e/cases/svg/svgr-query-react/index.test.ts
@@ -0,0 +1,24 @@
+import { expect, test } from '@playwright/test';
+import { build, gotoPage } from '@e2e/helper';
+
+test('should import default from SVG with react query correctly', async ({
+ page,
+}) => {
+ const rsbuild = await build({
+ cwd: __dirname,
+ runServer: true,
+ });
+
+ await gotoPage(page, rsbuild);
+
+ await expect(
+ page.evaluate(`document.getElementById('component').tagName === 'svg'`),
+ ).resolves.toBeTruthy();
+
+ // test svg asset
+ await expect(
+ page.evaluate(`document.getElementById('url').src`),
+ ).resolves.toMatch(/http:/);
+
+ await rsbuild.close();
+});
diff --git a/e2e/cases/svg/svgr-query-react/rsbuild.config.ts b/e2e/cases/svg/svgr-query-react/rsbuild.config.ts
new file mode 100644
index 0000000000..5d24b993b8
--- /dev/null
+++ b/e2e/cases/svg/svgr-query-react/rsbuild.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from '@rsbuild/core';
+import { pluginReact } from '@rsbuild/plugin-react';
+import { pluginSvgr } from '@rsbuild/plugin-svgr';
+
+export default defineConfig({
+ plugins: [pluginReact(), pluginSvgr()],
+});
diff --git a/e2e/cases/svg/svgr-query-react/src/App.jsx b/e2e/cases/svg/svgr-query-react/src/App.jsx
new file mode 100644
index 0000000000..badfc6e946
--- /dev/null
+++ b/e2e/cases/svg/svgr-query-react/src/App.jsx
@@ -0,0 +1,13 @@
+import url from './small.svg?url';
+import Component from './small.svg?react';
+
+function App() {
+ return (
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/e2e/cases/svg/svgr-query-react/src/index.js b/e2e/cases/svg/svgr-query-react/src/index.js
new file mode 100644
index 0000000000..5ceb026ccc
--- /dev/null
+++ b/e2e/cases/svg/svgr-query-react/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-query-react/src/small.svg b/e2e/cases/svg/svgr-query-react/src/small.svg
new file mode 100644
index 0000000000..2632a4809a
--- /dev/null
+++ b/e2e/cases/svg/svgr-query-react/src/small.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/core/src/plugins/asset.ts b/packages/core/src/plugins/asset.ts
index 717311671e..edf522fb8e 100644
--- a/packages/core/src/plugins/asset.ts
+++ b/packages/core/src/plugins/asset.ts
@@ -6,10 +6,50 @@ import {
IMAGE_EXTENSIONS,
VIDEO_EXTENSIONS,
AUDIO_EXTENSIONS,
- chainStaticAssetRule,
+ type BundlerChainRule,
} from '@rsbuild/shared';
import type { RsbuildPlugin } from '../types';
+const chainStaticAssetRule = ({
+ rule,
+ maxSize,
+ filename,
+ assetType,
+}: {
+ rule: BundlerChainRule;
+ maxSize: number;
+ filename: string;
+ assetType: string;
+}) => {
+ // force to url: "foo.png?url" or "foo.png?__inline=false"
+ rule
+ .oneOf(`${assetType}-asset-url`)
+ .type('asset/resource')
+ .resourceQuery(/(__inline=false|url)/)
+ .set('generator', {
+ filename,
+ });
+
+ // force to inline: "foo.png?inline"
+ rule
+ .oneOf(`${assetType}-asset-inline`)
+ .type('asset/inline')
+ .resourceQuery(/inline/);
+
+ // default: when size < dataUrlCondition.maxSize will inline
+ rule
+ .oneOf(`${assetType}-asset`)
+ .type('asset')
+ .parser({
+ dataUrlCondition: {
+ maxSize,
+ },
+ })
+ .set('generator', {
+ filename,
+ });
+};
+
export function getRegExpForExts(exts: string[]): RegExp {
const matcher = exts
.map((ext) => ext.trim())
diff --git a/packages/plugin-svgr/src/index.ts b/packages/plugin-svgr/src/index.ts
index 985682abe6..9f1677212d 100644
--- a/packages/plugin-svgr/src/index.ts
+++ b/packages/plugin-svgr/src/index.ts
@@ -5,7 +5,6 @@ import {
getDistPath,
getFilename,
SCRIPT_REGEX,
- chainStaticAssetRule,
} from '@rsbuild/shared';
import { PLUGIN_REACT_NAME } from '@rsbuild/plugin-react';
import type { RsbuildPlugin } from '@rsbuild/core';
@@ -53,52 +52,19 @@ export const pluginSvgr = (options: PluginSvgrOptions = {}): RsbuildPlugin => ({
setup(api) {
api.modifyBundlerChain(async (chain, { isProd, CHAIN_ID }) => {
const config = api.getNormalizedConfig();
-
const { svgDefaultExport = 'url' } = options;
- const assetType = 'svg';
- const distDir = getDistPath(config, assetType);
- const filename = getFilename(config, assetType, isProd);
+ const distDir = getDistPath(config, 'svg');
+ const filename = getFilename(config, 'svg', isProd);
const outputName = path.posix.join(distDir, filename);
const { dataUriLimit } = config.output;
const maxSize =
- typeof dataUriLimit === 'number'
- ? dataUriLimit
- : dataUriLimit[assetType];
+ typeof dataUriLimit === 'number' ? dataUriLimit : dataUriLimit.svg;
- // delete origin rules
+ // delete Rsbuild builtin SVG rules
chain.module.rules.delete(CHAIN_ID.RULE.SVG);
const rule = chain.module.rule(CHAIN_ID.RULE.SVG).test(SVG_REGEX);
- // If we import SVG from a CSS file, it will be processed as assets.
- chainStaticAssetRule({
- rule,
- maxSize,
- filename: path.posix.join(distDir, filename),
- assetType,
- issuer: {
- // The issuer option ensures that SVGR will only apply if the SVG is imported from a JS file.
- not: [SCRIPT_REGEX],
- },
- });
-
- const jsRule = chain.module.rules.get(CHAIN_ID.RULE.JS);
- const svgrRule = rule.oneOf(CHAIN_ID.ONE_OF.SVG).type('javascript/auto');
-
- [CHAIN_ID.USE.SWC, CHAIN_ID.USE.BABEL].some((id) => {
- const use = jsRule.uses.get(id);
-
- if (use) {
- svgrRule
- .use(id)
- .loader(use.get('loader'))
- .options(use.get('options'));
- return true;
- }
-
- return false;
- });
-
const svgrOptions = deepmerge(
{
svgo: true,
@@ -107,12 +73,65 @@ export const pluginSvgr = (options: PluginSvgrOptions = {}): RsbuildPlugin => ({
options.svgrOptions || {},
);
- svgrRule
+ // force to url: "foo.svg?url",
+ rule
+ .oneOf(CHAIN_ID.ONE_OF.SVG_URL)
+ .type('asset/resource')
+ .resourceQuery(/(__inline=false|url)/)
+ .set('generator', {
+ filename: outputName,
+ });
+
+ // force to inline: "foo.svg?inline"
+ rule
+ .oneOf(CHAIN_ID.ONE_OF.SVG_INLINE)
+ .type('asset/inline')
+ .resourceQuery(/inline/);
+
+ // force to react component: "foo.svg?react"
+ rule
+ .oneOf(CHAIN_ID.ONE_OF.SVG_REACT)
+ .type('javascript/auto')
+ .resourceQuery(/react/)
.use(CHAIN_ID.USE.SVGR)
.loader(path.resolve(__dirname, './loader'))
- .options(svgrOptions)
+ .options({
+ ...svgrOptions,
+ exportType: 'default',
+ } satisfies Config)
+ .end();
+
+ // SVG in non-JS files
+ // default: when size < dataUrlCondition.maxSize will inline
+ rule
+ .oneOf(CHAIN_ID.ONE_OF.SVG_ASSET)
+ .type('asset')
+ .parser({
+ dataUrlCondition: {
+ maxSize,
+ },
+ })
+ .set('generator', {
+ filename: outputName,
+ })
+ .set('issuer', {
+ // The issuer option ensures that SVGR will only apply if the SVG is imported from a JS file.
+ not: [SCRIPT_REGEX],
+ });
+
+ // SVG in JS files
+ const exportType = svgDefaultExport === 'url' ? 'named' : 'default';
+ rule
+ .oneOf(CHAIN_ID.ONE_OF.SVG)
+ .type('javascript/auto')
+ .use(CHAIN_ID.USE.SVGR)
+ .loader(path.resolve(__dirname, './loader'))
+ .options({
+ ...svgrOptions,
+ exportType,
+ })
.end()
- .when(svgDefaultExport === 'url', (c) =>
+ .when(exportType === 'named', (c) =>
c
.use(CHAIN_ID.USE.URL)
.loader(path.join(__dirname, '../compiled', 'url-loader'))
@@ -121,6 +140,30 @@ export const pluginSvgr = (options: PluginSvgrOptions = {}): RsbuildPlugin => ({
name: outputName,
}),
);
+
+ // apply current JS transform rule to SVGR rules
+ const jsRule = chain.module.rules.get(CHAIN_ID.RULE.JS);
+
+ [CHAIN_ID.USE.SWC, CHAIN_ID.USE.BABEL].some((jsUseId) => {
+ const use = jsRule.uses.get(jsUseId);
+
+ if (use) {
+ [CHAIN_ID.ONE_OF.SVG, CHAIN_ID.ONE_OF.SVG_REACT].forEach(
+ (oneOfId) => {
+ rule
+ .oneOf(oneOfId)
+ .use(jsUseId)
+ .before(CHAIN_ID.USE.SVGR)
+ .loader(use.get('loader'))
+ .options(use.get('options'));
+ },
+ );
+
+ return true;
+ }
+
+ return false;
+ });
});
},
});
diff --git a/packages/plugin-svgr/tests/__snapshots__/index.test.ts.snap b/packages/plugin-svgr/tests/__snapshots__/index.test.ts.snap
index f472205c06..09a0efb9b3 100644
--- a/packages/plugin-svgr/tests/__snapshots__/index.test.ts.snap
+++ b/packages/plugin-svgr/tests/__snapshots__/index.test.ts.snap
@@ -14,6 +14,70 @@ exports[`svgr > 'configure SVGR options' 1`] = `
"resourceQuery": /inline/,
"type": "asset/inline",
},
+ {
+ "resourceQuery": /react/,
+ "type": "javascript/auto",
+ "use": [
+ {
+ "loader": "builtin:swc-loader",
+ "options": {
+ "env": {
+ "coreJs": "3.32",
+ "mode": "usage",
+ "shippedProposals": true,
+ "targets": [
+ "chrome >= 87",
+ "edge >= 88",
+ "firefox >= 78",
+ "safari >= 14",
+ ],
+ },
+ "isModule": "unknown",
+ "jsc": {
+ "externalHelpers": true,
+ "parser": {
+ "decorators": true,
+ "syntax": "typescript",
+ "tsx": true,
+ },
+ "preserveAllComments": true,
+ "transform": {
+ "decoratorMetadata": true,
+ "legacyDecorator": true,
+ "react": {
+ "development": false,
+ "refresh": true,
+ "runtime": "automatic",
+ },
+ "useDefineForClassFields": false,
+ },
+ },
+ "sourceMaps": true,
+ },
+ },
+ {
+ "loader": "/packages/plugin-svgr/src/loader",
+ "options": {
+ "exportType": "default",
+ "svgo": true,
+ "svgoConfig": {
+ "datauri": "base64",
+ "plugins": [
+ {
+ "name": "preset-default",
+ "params": {
+ "overrides": {
+ "removeViewBox": false,
+ },
+ },
+ },
+ "prefixIds",
+ ],
+ },
+ },
+ },
+ ],
+ },
{
"generator": {
"filename": "static/svg/[name].[contenthash:8].svg",
@@ -73,6 +137,7 @@ exports[`svgr > 'configure SVGR options' 1`] = `
{
"loader": "/packages/plugin-svgr/src/loader",
"options": {
+ "exportType": "named",
"svgo": true,
"svgoConfig": {
"datauri": "base64",
@@ -118,6 +183,69 @@ exports[`svgr > 'export default Component' 1`] = `
"resourceQuery": /inline/,
"type": "asset/inline",
},
+ {
+ "resourceQuery": /react/,
+ "type": "javascript/auto",
+ "use": [
+ {
+ "loader": "builtin:swc-loader",
+ "options": {
+ "env": {
+ "coreJs": "3.32",
+ "mode": "usage",
+ "shippedProposals": true,
+ "targets": [
+ "chrome >= 87",
+ "edge >= 88",
+ "firefox >= 78",
+ "safari >= 14",
+ ],
+ },
+ "isModule": "unknown",
+ "jsc": {
+ "externalHelpers": true,
+ "parser": {
+ "decorators": true,
+ "syntax": "typescript",
+ "tsx": true,
+ },
+ "preserveAllComments": true,
+ "transform": {
+ "decoratorMetadata": true,
+ "legacyDecorator": true,
+ "react": {
+ "development": false,
+ "refresh": true,
+ "runtime": "automatic",
+ },
+ "useDefineForClassFields": false,
+ },
+ },
+ "sourceMaps": true,
+ },
+ },
+ {
+ "loader": "/packages/plugin-svgr/src/loader",
+ "options": {
+ "exportType": "default",
+ "svgo": true,
+ "svgoConfig": {
+ "plugins": [
+ {
+ "name": "preset-default",
+ "params": {
+ "overrides": {
+ "removeViewBox": false,
+ },
+ },
+ },
+ "prefixIds",
+ ],
+ },
+ },
+ },
+ ],
+ },
{
"generator": {
"filename": "static/svg/[name].[contenthash:8].svg",
@@ -177,6 +305,7 @@ exports[`svgr > 'export default Component' 1`] = `
{
"loader": "/packages/plugin-svgr/src/loader",
"options": {
+ "exportType": "default",
"svgo": true,
"svgoConfig": {
"plugins": [
@@ -214,6 +343,69 @@ exports[`svgr > 'export default url' 1`] = `
"resourceQuery": /inline/,
"type": "asset/inline",
},
+ {
+ "resourceQuery": /react/,
+ "type": "javascript/auto",
+ "use": [
+ {
+ "loader": "builtin:swc-loader",
+ "options": {
+ "env": {
+ "coreJs": "3.32",
+ "mode": "usage",
+ "shippedProposals": true,
+ "targets": [
+ "chrome >= 87",
+ "edge >= 88",
+ "firefox >= 78",
+ "safari >= 14",
+ ],
+ },
+ "isModule": "unknown",
+ "jsc": {
+ "externalHelpers": true,
+ "parser": {
+ "decorators": true,
+ "syntax": "typescript",
+ "tsx": true,
+ },
+ "preserveAllComments": true,
+ "transform": {
+ "decoratorMetadata": true,
+ "legacyDecorator": true,
+ "react": {
+ "development": false,
+ "refresh": true,
+ "runtime": "automatic",
+ },
+ "useDefineForClassFields": false,
+ },
+ },
+ "sourceMaps": true,
+ },
+ },
+ {
+ "loader": "/packages/plugin-svgr/src/loader",
+ "options": {
+ "exportType": "default",
+ "svgo": true,
+ "svgoConfig": {
+ "plugins": [
+ {
+ "name": "preset-default",
+ "params": {
+ "overrides": {
+ "removeViewBox": false,
+ },
+ },
+ },
+ "prefixIds",
+ ],
+ },
+ },
+ },
+ ],
+ },
{
"generator": {
"filename": "static/svg/[name].[contenthash:8].svg",
@@ -273,6 +465,7 @@ exports[`svgr > 'export default url' 1`] = `
{
"loader": "/packages/plugin-svgr/src/loader",
"options": {
+ "exportType": "named",
"svgo": true,
"svgoConfig": {
"plugins": [
diff --git a/packages/shared/src/chain.ts b/packages/shared/src/chain.ts
index 2510a0f50b..7cd228fa6f 100644
--- a/packages/shared/src/chain.ts
+++ b/packages/shared/src/chain.ts
@@ -112,6 +112,7 @@ export const CHAIN_ID = {
SVG: 'svg',
SVG_URL: 'svg-asset-url',
SVG_ASSET: 'svg-asset',
+ SVG_REACT: 'svg-react',
SVG_INLINE: 'svg-asset-inline',
},
/** Predefined loaders */
@@ -134,7 +135,7 @@ export const CHAIN_ID = {
VUE: 'vue',
/** swc-loader */
SWC: 'swc',
- /** @svgr/webpack */
+ /** svgr */
SVGR: 'svgr',
/** plugin-image-compress svgo-loader */
SVGO: 'svgo',
diff --git a/packages/shared/src/config.ts b/packages/shared/src/config.ts
index 41d80dbaea..f6bea3c32d 100644
--- a/packages/shared/src/config.ts
+++ b/packages/shared/src/config.ts
@@ -1,6 +1,5 @@
import type {
RsbuildConfig,
- BundlerChainRule,
NormalizedConfig,
InspectConfigOptions,
} from './types';
@@ -10,7 +9,6 @@ import type { minify } from 'terser';
import fse from '../compiled/fs-extra';
import { pick, color, upperFirst, deepmerge } from './utils';
import { getTerserMinifyOptions } from './minimize';
-import type { RuleSetCondition } from '@rspack/core';
import { parseMinifyOptions } from './minimize';
export async function outputInspectConfigFiles({
@@ -137,50 +135,6 @@ export async function stringifyConfig(config: unknown, verbose?: boolean) {
return stringify(config as any, { verbose });
}
-export const chainStaticAssetRule = ({
- rule,
- maxSize,
- filename,
- assetType,
- issuer,
-}: {
- rule: BundlerChainRule;
- maxSize: number;
- filename: string;
- assetType: string;
- issuer?: RuleSetCondition;
-}) => {
- // Rspack not support dataUrlCondition function
- // forceNoInline: "foo.png?__inline=false" or "foo.png?url",
- rule
- .oneOf(`${assetType}-asset-url`)
- .type('asset/resource')
- .resourceQuery(/(__inline=false|url)/)
- .set('generator', {
- filename,
- });
-
- // forceInline: "foo.png?inline" or "foo.png?__inline",
- rule
- .oneOf(`${assetType}-asset-inline`)
- .type('asset/inline')
- .resourceQuery(/inline/);
-
- // default: when size < dataUrlCondition.maxSize will inline
- rule
- .oneOf(`${assetType}-asset`)
- .type('asset')
- .parser({
- dataUrlCondition: {
- maxSize,
- },
- })
- .set('generator', {
- filename,
- })
- .set('issuer', issuer);
-};
-
export const getDefaultStyledComponentsConfig = (
isProd: boolean,
ssr: boolean,