From e66fe84e3b2693517d3a4d674d2c5c602c15d852 Mon Sep 17 00:00:00 2001 From: neverland Date: Mon, 11 Mar 2024 14:13:58 +0800 Subject: [PATCH] feat(plugin-svgr): support for ?react query --- e2e/cases/svg/svgr-query-react/index.test.ts | 24 +++ .../svg/svgr-query-react/rsbuild.config.ts | 7 + e2e/cases/svg/svgr-query-react/src/App.jsx | 13 ++ e2e/cases/svg/svgr-query-react/src/index.js | 9 + e2e/cases/svg/svgr-query-react/src/small.svg | 3 + packages/core/src/plugins/asset.ts | 42 +++- packages/plugin-svgr/src/index.ts | 125 ++++++++---- .../tests/__snapshots__/index.test.ts.snap | 193 ++++++++++++++++++ packages/shared/src/chain.ts | 3 +- packages/shared/src/config.ts | 46 ----- 10 files changed, 376 insertions(+), 89 deletions(-) create mode 100644 e2e/cases/svg/svgr-query-react/index.test.ts create mode 100644 e2e/cases/svg/svgr-query-react/rsbuild.config.ts create mode 100644 e2e/cases/svg/svgr-query-react/src/App.jsx create mode 100644 e2e/cases/svg/svgr-query-react/src/index.js create mode 100644 e2e/cases/svg/svgr-query-react/src/small.svg 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 ( +
+ + url +
+ ); +} + +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,