UNPKG

@nx/react

Version:

The React plugin for Nx contains executors and generators for managing React applications and libraries within an Nx workspace. It provides: - Integration with libraries such as Jest, Vitest, Playwright, Cypress, and Storybook. - Generators for applica

438 lines (427 loc) • 20.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = addSvgrToWebpackConfig; const tslib_1 = require("tslib"); const devkit_1 = require("@nx/devkit"); const executor_options_utils_1 = require("@nx/devkit/src/generators/executor-options-utils"); const tsquery_1 = require("@phenomnomnominal/tsquery"); const ts = tslib_1.__importStar(require("typescript")); const withSvgrFunctionForWithReact = ` // SVGR support function (migrated from svgr option in withReact/NxReactWebpackPlugin) function withSvgr(svgrOptions = {}) { const defaultOptions = { svgo: false, titleProp: true, ref: true, }; const options = { ...defaultOptions, ...svgrOptions }; return function configure(config) { // Remove existing SVG loader if present const svgLoaderIdx = config.module.rules.findIndex( (rule) => typeof rule === 'object' && typeof rule.test !== 'undefined' && rule.test.toString().includes('svg') ); if (svgLoaderIdx !== -1) { config.module.rules.splice(svgLoaderIdx, 1); } // Add SVGR loader with webpack 5 asset modules config.module.rules.push({ test: /\\.svg$/, oneOf: [ { resourceQuery: /url/, type: 'asset/resource', generator: { filename: '[name].[hash][ext]', }, }, { issuer: /\\.(js|ts|md)x?$/, use: [ { loader: require.resolve('@svgr/webpack'), options, }, ], }, ], }); return config; }; } `; const withSvgrFunctionForNxReactWebpackPlugin = ` // SVGR support function (migrated from svgr option in withReact/NxReactWebpackPlugin) function withSvgr(svgrOptions = {}) { const defaultOptions = { svgo: false, titleProp: true, ref: true, }; const options = { ...defaultOptions, ...svgrOptions }; return (config) => { config.plugins.push({ apply: (compiler) => { // Remove ALL existing SVG loaders compiler.options.module.rules = compiler.options.module.rules.filter( (rule) => !( rule && typeof rule === 'object' && rule.test && rule.test.toString().includes('svg') ) ); // Add SVGR loader with webpack 5 asset modules compiler.options.module.rules.push({ test: /\.svg$/, oneOf: [ { resourceQuery: /url/, type: 'asset/resource', generator: { filename: '[name].[hash][ext]', }, }, { issuer: /\.[jt]sx?$/, use: [ { loader: require.resolve('@svgr/webpack'), options, }, ], }, ], }); }, }); return config; }; } `; async function addSvgrToWebpackConfig(tree) { const projects = new Map(); // Find all React webpack projects using either withReact OR NxReactWebpackPlugin (0, executor_options_utils_1.forEachExecutorOptions)(tree, '@nx/webpack:webpack', (options, project, target) => { if (!options.webpackConfig) return; const webpackConfigPath = options.webpackConfig; if (!tree.exists(webpackConfigPath)) return; const content = tree.read(webpackConfigPath, 'utf-8'); const sourceFile = (0, tsquery_1.ast)(content); // Check if this is a withReact setup if (content.includes('withReact')) { const withReactCalls = (0, tsquery_1.query)(sourceFile, 'CallExpression[expression.name=withReact]'); if (withReactCalls.length > 0) { const callExpr = withReactCalls[0]; if (callExpr.arguments.length === 0) return; const arg = callExpr.arguments[0]; if (!ts.isObjectLiteralExpression(arg)) return; const svgrProp = arg.properties.find((prop) => ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'svgr'); if (svgrProp) { let svgrValue; if (ts.isObjectLiteralExpression(svgrProp.initializer)) { svgrValue = {}; for (const prop of svgrProp.initializer.properties) { if (!ts.isPropertyAssignment(prop)) continue; if (!ts.isIdentifier(prop.name)) continue; const key = prop.name.text; if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) { svgrValue[key] = true; } else if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) { svgrValue[key] = false; } } } else { svgrValue = svgrProp.initializer.kind === ts.SyntaxKind.TrueKeyword; } projects.set(webpackConfigPath, { svgrOptions: svgrValue, isWithReact: true, }); } } } // Otherwise check if this is NxReactWebpackPlugin setup else if (content.includes('NxReactWebpackPlugin')) { const pluginCalls = (0, tsquery_1.query)(sourceFile, 'NewExpression[expression.name=NxReactWebpackPlugin]'); if (pluginCalls.length > 0) { const newExpr = pluginCalls[0]; if (!newExpr.arguments || newExpr.arguments.length === 0) return; const arg = newExpr.arguments[0]; if (!ts.isObjectLiteralExpression(arg)) return; const svgrProp = arg.properties.find((prop) => ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'svgr'); if (svgrProp) { let svgrValue; if (ts.isObjectLiteralExpression(svgrProp.initializer)) { svgrValue = {}; for (const prop of svgrProp.initializer.properties) { if (!ts.isPropertyAssignment(prop)) continue; if (!ts.isIdentifier(prop.name)) continue; const key = prop.name.text; if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) { svgrValue[key] = true; } else if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) { svgrValue[key] = false; } } } else if (svgrProp.initializer.kind === ts.SyntaxKind.TrueKeyword) { svgrValue = true; } else if (svgrProp.initializer.kind === ts.SyntaxKind.FalseKeyword) { svgrValue = false; } // Add to projects if svgr is explicitly set if (svgrValue !== undefined) { projects.set(webpackConfigPath, { svgrOptions: svgrValue, isWithReact: false, }); } } } } }); if (projects.size === 0) return; // Update webpack configs to add withSvgr function inline for (const [webpackConfigPath, config] of projects.entries()) { let content = tree.read(webpackConfigPath, 'utf-8'); const sourceFile = (0, tsquery_1.ast)(content); const changes = []; // Build the svgr options for this specific config let svgrOptionsStr = ''; if (config.svgrOptions) { const importStatements = (0, tsquery_1.query)(sourceFile, 'ImportDeclaration'); const requireStatements = (0, tsquery_1.query)(sourceFile, 'VariableStatement:has(CallExpression[expression.name=require])'); const allImportRequires = [ ...importStatements, ...requireStatements, ].sort((a, b) => a.getEnd() - b.getEnd()); const lastImportOrRequire = allImportRequires[allImportRequires.length - 1]; if (config.svgrOptions === true || config.svgrOptions === undefined) { svgrOptionsStr = ''; } else if (typeof config.svgrOptions === 'object') { const options = Object.entries(config.svgrOptions) .map(([key, value]) => ` ${key}: ${value}`) .join(',\n'); svgrOptionsStr = `{\n${options}\n}`; } if (lastImportOrRequire) { changes.push({ type: devkit_1.ChangeType.Insert, index: lastImportOrRequire.getEnd(), text: config.isWithReact ? withSvgrFunctionForWithReact : withSvgrFunctionForNxReactWebpackPlugin, }); } else { changes.push({ type: devkit_1.ChangeType.Insert, index: 0, text: (config.isWithReact ? withSvgrFunctionForWithReact : withSvgrFunctionForNxReactWebpackPlugin) + '\n', }); } } // Remove svgr option based on the style (withReact OR NxReactWebpackPlugin) if (config.isWithReact) { // Remove svgr option from first withReact call (only one expected) const withReactCalls = (0, tsquery_1.query)(sourceFile, 'CallExpression[expression.name=withReact]'); if (withReactCalls.length > 0) { const callExpr = withReactCalls[0]; if (callExpr.arguments.length > 0) { const arg = callExpr.arguments[0]; if (ts.isObjectLiteralExpression(arg)) { const svgrProp = arg.properties.find((prop) => ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'svgr'); if (svgrProp) { const hasOnlySvgrProperty = arg.properties.length === 1; if (hasOnlySvgrProperty) { changes.push({ type: devkit_1.ChangeType.Delete, start: arg.getStart(), length: arg.getEnd() - arg.getStart(), }); } else { const propIndex = arg.properties.indexOf(svgrProp); const isLastProp = propIndex === arg.properties.length - 1; const isFirstProp = propIndex === 0; let removeStart = svgrProp.getFullStart(); let removeEnd = svgrProp.getEnd(); if (!isLastProp) { const nextProp = arg.properties[propIndex + 1]; removeEnd = nextProp.getFullStart(); } else if (!isFirstProp) { const prevProp = arg.properties[propIndex - 1]; const textBetween = content.substring(prevProp.getEnd(), svgrProp.getFullStart()); const commaIndex = textBetween.indexOf(','); if (commaIndex !== -1) { removeStart = prevProp.getEnd() + commaIndex; } } changes.push({ type: devkit_1.ChangeType.Delete, start: removeStart, length: removeEnd - removeStart, }); } if (config.svgrOptions) { const composePluginsCalls = (0, tsquery_1.query)(sourceFile, 'CallExpression[expression.name=composePlugins]'); if (composePluginsCalls.length > 0) { const composeCall = composePluginsCalls[0]; let svgrCallStr = ''; if (config.svgrOptions === true || config.svgrOptions === undefined) { svgrCallStr = 'withSvgr()'; } else if (typeof config.svgrOptions === 'object') { svgrCallStr = `withSvgr(${svgrOptionsStr})`; } const withReactIdx = composeCall.arguments.findIndex((arg) => arg.getText().includes('withReact')); // Insert withSvgr as the last argument before the closing paren const argToInsertAfter = composeCall.arguments[withReactIdx]; if (argToInsertAfter) { changes.push({ type: devkit_1.ChangeType.Insert, index: argToInsertAfter.getEnd(), text: `, ${svgrCallStr}`, }); } } } } } } } } else { // Remove svgr option from first NxReactWebpackPlugin call const pluginCalls = (0, tsquery_1.query)(sourceFile, 'NewExpression[expression.name=NxReactWebpackPlugin]'); if (pluginCalls.length > 0) { const newExpr = pluginCalls[0]; if (newExpr.arguments && newExpr.arguments.length > 0) { const arg = newExpr.arguments[0]; if (ts.isObjectLiteralExpression(arg)) { const svgrProp = arg.properties.find((prop) => ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'svgr'); if (svgrProp) { const hasOnlySvgrProperty = arg.properties.length === 1; // Replace entire object argument with empty parentheses when only svgr property exists if (hasOnlySvgrProperty) { changes.push({ type: devkit_1.ChangeType.Delete, start: arg.getStart(), length: arg.getEnd() - arg.getStart(), }); } // If more properties exist, just remove the svgr property else { const propIndex = arg.properties.indexOf(svgrProp); const isLastProp = propIndex === arg.properties.length - 1; const isFirstProp = propIndex === 0; let removeStart = svgrProp.getFullStart(); let removeEnd = svgrProp.getEnd(); if (!isLastProp) { const nextProp = arg.properties[propIndex + 1]; removeEnd = nextProp.getFullStart(); } else if (!isFirstProp) { const prevProp = arg.properties[propIndex - 1]; const textBetween = content.substring(prevProp.getEnd(), svgrProp.getFullStart()); const commaIndex = textBetween.indexOf(','); if (commaIndex !== -1) { removeStart = prevProp.getEnd() + commaIndex; } } changes.push({ type: devkit_1.ChangeType.Delete, start: removeStart, length: removeEnd - removeStart, }); } } } } } // For NxReactWebpackPlugin style, wrap the entire module.exports or export default with withSvgr if (config.svgrOptions) { const allAssignments = (0, tsquery_1.query)(sourceFile, 'BinaryExpression'); // Find the one that has module.exports on the left side const moduleExportsAssignment = allAssignments.find((node) => { const binaryExpr = node; const left = binaryExpr.left; return (ts.isPropertyAccessExpression(left) && ts.isIdentifier(left.expression) && left.expression.text === 'module' && ts.isIdentifier(left.name) && left.name.text === 'exports'); }); // Also check for export default const exportDefaultStatements = (0, tsquery_1.query)(sourceFile, 'ExportAssignment'); const exportDefaultStatement = exportDefaultStatements[0]; // Use whichever export style is found let exportValue; if (moduleExportsAssignment) { exportValue = moduleExportsAssignment.right; } else if (exportDefaultStatement) { exportValue = exportDefaultStatement.expression; } if (exportValue) { let svgrCallStr = ''; if (config.svgrOptions === true || config.svgrOptions === undefined) { svgrCallStr = 'withSvgr()'; } else if (typeof config.svgrOptions === 'object') { const options = Object.entries(config.svgrOptions) .map(([key, value]) => ` ${key}: ${value}`) .join(',\n'); svgrCallStr = `withSvgr({\n${options}\n})`; } changes.push({ type: devkit_1.ChangeType.Insert, index: exportValue.getStart(), text: `${svgrCallStr}(`, }); changes.push({ type: devkit_1.ChangeType.Insert, index: exportValue.getEnd(), text: ')', }); } } } content = (0, devkit_1.applyChangesToString)(content, changes); tree.write(webpackConfigPath, content); } await (0, devkit_1.formatFiles)(tree); }