From a9fe4174359ac7e5ab46c501f4d7064ab074dae2 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Thu, 9 May 2024 16:14:28 -0600 Subject: [PATCH] feat(react): Add SvgOptions for NxReactWebpackPlugin and WithNx closes: #9487 --- e2e/next/src/next-svgr.test.ts | 63 ++++++++++++++ e2e/react/src/react-webpack.test.ts | 83 +++++++++++++++++++ packages/next/plugins/with-nx.ts | 24 ++++-- .../lib/apply-react-config.ts | 20 +++-- packages/react/plugins/with-react.ts | 7 +- 5 files changed, 182 insertions(+), 15 deletions(-) diff --git a/e2e/next/src/next-svgr.test.ts b/e2e/next/src/next-svgr.test.ts index e7ab9d5226a7e..22b640e4e41d7 100644 --- a/e2e/next/src/next-svgr.test.ts +++ b/e2e/next/src/next-svgr.test.ts @@ -77,4 +77,67 @@ describe('NextJs SVGR support', () => { ); expect(svgFile).toBeTruthy(); }); + + it('should allow both SVG asset and SVGR component to be used (using SvgrOptions)', () => { + const appName = uniq('app'); + runCLI( + `generate @nx/next:app ${appName} --no-interactive --appDir=true --src=true` + ); + createFile( + `apps/${appName}/src/app/nx.svg`, + ` + + SVG for app + + ` + ); + updateFile( + `apps/${appName}/src/app/page.tsx`, + ` + import Image from 'next/image'; + import svgImg, { ReactComponent as Logo } from './nx.svg'; + export default async function Index() { + return ( + <> + Alt for SVG img tag + + + ); + } + ` + ); + updateFile( + `apps/${appName}/next.config.js`, + ` + const { composePlugins, withNx } = require('@nx/next'); + const nextConfig = { + nx: { + svgr: { + svgo: false, + titleProp: true, + ref: true, + }, + }, + }; + const plugins = [ + withNx, + ]; + module.exports = composePlugins(...plugins)(nextConfig); + ` + ); + + runCLI(`build ${appName}`); + + const pageFile = readFile(`apps/${appName}/.next/server/app/page.js`); + const svgFile = listFiles(`apps/${appName}/.next/static/media`).find((f) => + /nx\.[a-z0-9]+\.svg$/.test(f) + ); + expect(`apps/${appName}/.next/static/chunks/app/${pageFile}`).toMatch( + /SVG for app/ + ); + expect(`apps/${appName}/.next/static/chunks/app/${pageFile}`).toMatch( + /Alt for SVG img tag/ + ); + expect(svgFile).toBeTruthy(); + }); }); diff --git a/e2e/react/src/react-webpack.test.ts b/e2e/react/src/react-webpack.test.ts index f031bed41feb3..aec45262ef34f 100644 --- a/e2e/react/src/react-webpack.test.ts +++ b/e2e/react/src/react-webpack.test.ts @@ -62,4 +62,87 @@ describe('Build React applications and libraries with Vite', () => { expect(mainContent).toMatch(/Alt for SVG img tag/); expect(svgFile).toBeTruthy(); }, 300_000); + + it('should support SVGR and SVG asset in the same project (using SvgrOptions)', async () => { + const appName = uniq('app'); + + runCLI( + `generate @nx/react:app ${appName} --bundler=webpack --compiler=babel --unitTestRunner=none --no-interactive` + ); + createFile( + `apps/${appName}/src/app/nx.svg`, + ` + + SVG for app + + ` + ); + updateFile( + `apps/${appName}/src/app/app.tsx`, + ` + import svgImg, { ReactComponent as Logo } from './nx.svg'; + export function App() { + return ( + <> + Alt for SVG img tag + + + ); + } + export default App; + ` + ); + + updateFile( + `apps/${appName}/webpack.config.js`, + ` + const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); + const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin'); + const { join } = require('path'); + + module.exports = { + output: { + path: join(__dirname, '../../dist/apps/${appName}'), + }, + devServer: { + port: 4201, + }, + plugins: [ + new NxAppWebpackPlugin({ + tsConfig: './tsconfig.app.json', + compiler: 'babel', + main: './src/main.tsx', + index: './src/index.html', + baseHref: '/', + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.css'], + outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none', + optimization: process.env['NODE_ENV'] === 'production', + }), + new NxReactWebpackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + svgr: { + svgo: false, + titleProp: true, + ref: true, + }, + }), + ], + }; + + + ` + ); + + await runCLIAsync(`build ${appName}`); + + const outFiles = listFiles(`dist/apps/${appName}`); + const mainFile = outFiles.find((f) => f.startsWith('main.')); + const mainContent = readFile(`dist/apps/${appName}/${mainFile}`); + const svgFile = outFiles.find((f) => f.endsWith('.svg')); + expect(mainContent).toMatch(/SVG for app/); + expect(mainContent).toMatch(/Alt for SVG img tag/); + expect(svgFile).toBeTruthy(); + }, 300_000); }); diff --git a/packages/next/plugins/with-nx.ts b/packages/next/plugins/with-nx.ts index c9536b070d942..8f3eb34129a78 100644 --- a/packages/next/plugins/with-nx.ts +++ b/packages/next/plugins/with-nx.ts @@ -12,9 +12,15 @@ import { type Target, } from '@nx/devkit'; +export interface SvgrOptions { + svgo?: boolean; + titleProp?: boolean; + ref?: boolean; +} + export interface WithNxOptions extends NextConfig { nx?: { - svgr?: boolean; + svgr?: boolean | SvgrOptions; babelUpwardRootMode?: boolean; }; } @@ -331,7 +337,15 @@ export function getNextConfig( */ // Default SVGR support to be on for projects. - if (nx?.svgr !== false) { + if (nx?.svgr !== false || typeof nx?.svgr === 'object') { + const defaultSvgrOptions = { + svgo: false, + titleProp: true, + ref: true, + }; + + const svgrOptions = + typeof nx?.svgr === 'object' ? nx.svgr : defaultSvgrOptions; // TODO(v20): Remove file-loader and use `?react` querystring to differentiate between asset and SVGR. // It should be: // use: [{ @@ -365,11 +379,7 @@ export function getNextConfig( use: [ { loader: require.resolve('@svgr/webpack'), - options: { - svgo: false, - titleProp: true, - ref: true, - }, + options: svgrOptions, }, { loader: require.resolve('file-loader'), diff --git a/packages/react/plugins/nx-react-webpack-plugin/lib/apply-react-config.ts b/packages/react/plugins/nx-react-webpack-plugin/lib/apply-react-config.ts index fc3ad55fc81ca..0bb46af7b3fe2 100644 --- a/packages/react/plugins/nx-react-webpack-plugin/lib/apply-react-config.ts +++ b/packages/react/plugins/nx-react-webpack-plugin/lib/apply-react-config.ts @@ -1,16 +1,26 @@ import { Configuration, WebpackOptionsNormalized } from 'webpack'; +import { SvgrOptions } from '../../with-react'; export function applyReactConfig( - options: { svgr?: boolean }, + options: { svgr?: boolean | SvgrOptions }, config: Partial = {} ): void { if (!process.env['NX_TASK_TARGET_PROJECT']) return; addHotReload(config); - if (options.svgr !== false) { + if (options.svgr !== false || typeof options.svgr === 'object') { removeSvgLoaderIfPresent(config); + const defaultSvgrOptions = { + svgo: false, + titleProp: true, + ref: true, + }; + + const svgrOptions = + typeof options.svgr === 'object' ? options.svgr : defaultSvgrOptions; + // TODO(v20): Remove file-loader and use `?react` querystring to differentiate between asset and SVGR. // It should be: // use: [{ @@ -37,11 +47,7 @@ export function applyReactConfig( use: [ { loader: require.resolve('@svgr/webpack'), - options: { - svgo: false, - titleProp: true, - ref: true, - }, + options: svgrOptions, }, { loader: require.resolve('file-loader'), diff --git a/packages/react/plugins/with-react.ts b/packages/react/plugins/with-react.ts index 47ae082a23bd8..ac3e5fdec4995 100644 --- a/packages/react/plugins/with-react.ts +++ b/packages/react/plugins/with-react.ts @@ -4,8 +4,13 @@ import { applyReactConfig } from './nx-react-webpack-plugin/lib/apply-react-conf const processed = new Set(); +export interface SvgrOptions { + svgo?: boolean; + titleProp?: boolean; + ref?: boolean; +} export interface WithReactOptions extends WithWebOptions { - svgr?: false; + svgr?: boolean | SvgrOptions; } /**