diff --git a/.changeset/chore-add-optimization-pass-in-svg-convertor.md b/.changeset/chore-add-optimization-pass-in-svg-convertor.md new file mode 100644 index 000000000..bb9c74cc4 --- /dev/null +++ b/.changeset/chore-add-optimization-pass-in-svg-convertor.md @@ -0,0 +1,15 @@ +--- +'@clickhouse/click-ui': patch +--- + +Add an SVG normalization step to fix breaking issues before proceeding with conversion and optimization. The normalization is based in "conservative" optimisation steps, to reduce chances of visual changes. + +**Before:** +``` +SVG File → SVGR (SVGO) → React Component +``` + +**After:** +``` +SVG File → SVGO Normalize → Create temp file → SVGR (no SVGO) → React Component → Delete temp file +``` diff --git a/.gitignore b/.gitignore index db827b233..5a6c8a8ff 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ storybook-static vite.config.ts.timestamp* .storybook/out tmp/* +.scripts/js/.temp/ .yarn/* !.yarn/releases diff --git a/.scripts/js/convert-svg-to-react-component b/.scripts/js/convert-svg-to-react-component index d4fec3535..de763ae08 100755 --- a/.scripts/js/convert-svg-to-react-component +++ b/.scripts/js/convert-svg-to-react-component @@ -8,6 +8,7 @@ import fs from 'fs'; import path from 'path'; import { execSync } from 'child_process'; import { fileURLToPath } from 'url'; +import { optimize } from 'svgo'; import { filenameToKebabCase, filenameToComponentName, @@ -136,8 +137,65 @@ const regenerateAssetRegistry = (config) => { ); }; -const convertSvgToComponent = (svgPath, componentName, outputPath, defaultSize) => { - const svgrConfig = path.join(__dirname, '../..', '.svgrrc.mjs'); +const normalizeSvg = (svgPath, tempPath) => { + console.log('🔧 Normalizing SVG...'); + + try { + const svgContent = fs.readFileSync(svgPath, 'utf-8'); + + const result = optimize(svgContent, { + multipass: true, + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + convertPathData: false + } + } + } + ] + }); + + fs.writeFileSync(tempPath, result.data); + console.log('✅ SVG normalized successfully'); + return tempPath; + } catch (error) { + console.error('👹 Oops! SVG normalization failed:', error.message); + throw error; + } +}; + +const convertSvgToComponent = (svgPath, componentName, outputPath, defaultSize, preNormalized = false) => { + let svgrConfig; + let tempConfigPath = null; + + if (preNormalized) { + tempConfigPath = path.join(__dirname, '.temp', `svgr-config-${Date.now()}.mjs`); + const configContent = `export default { + typescript: true, + ref: false, + memo: false, + svgo: false, + template: (variables, { tpl }) => { + return tpl\` +import { SVGAssetProps } from '../types'; + +const \${variables.componentName} = (props: SVGAssetProps) => ( + \${variables.jsx} +); + +export default \${variables.componentName}; +\`; + }, +};`; + fs.writeFileSync(tempConfigPath, configContent); + svgrConfig = tempConfigPath; + } else { + svgrConfig = path.join(__dirname, '../..', '.svgrrc.mjs'); + } + const cmd = `npx @svgr/cli --config-file "${svgrConfig}" --typescript "${svgPath}"`; let output; @@ -175,6 +233,13 @@ const convertSvgToComponent = (svgPath, componentName, outputPath, defaultSize) }); fs.writeFileSync(outputPath, output); + + if (tempConfigPath && fs.existsSync(tempConfigPath)) { + try { + fs.unlinkSync(tempConfigPath); + } catch (error) { + } + } }; const cleanUp = (files) => { @@ -204,6 +269,7 @@ const cleanUp = (files) => { let args = process.argv.slice(2); const isRegenerate = args.includes('--regenerate'); +const skipNormalize = args.includes('--skip-normalize'); // WARN: Do not change as this extracts and removes the --type flag from args if present, to allow args in package.json scripts const typeFlagIndex = args.findIndex(arg => arg.startsWith('--type=')); @@ -213,6 +279,10 @@ if (typeFlagIndex !== -1) { args = args.filter((_, i) => i !== typeFlagIndex); } +if (skipNormalize) { + args = args.filter(arg => arg !== '--skip-normalize'); +} + if (isRegenerate) { let typesToRegenerate; let titleMessage; @@ -257,8 +327,9 @@ if (isRegenerate) { if (args.length < 1) { console.error('👹 Oops! The SVG file path is required'); - console.error('💡 Usage: convert-svg-to-react-component [component-name] [--type=logos|icons|flags]'); + console.error('💡 Usage: convert-svg-to-react-component [component-name] [--type=logos|icons|flags] [--skip-normalize]'); console.error('💡 Or use: convert-svg-to-react-component --regenerate [--type=logos|icons|flags|payments]'); + console.error('💡 Note: SVGs are automatically normalized before conversion. Use --skip-normalize to disable.'); process.exit(1); } @@ -296,7 +367,37 @@ const darkFile = path.join(systemDir, `${config.registryName.replace('Light', 'D console.log(`🍩 Baking ${componentName} for ${type}...`); -convertSvgToComponent(svgPath, componentName, outputPath, config.defaultSize); +let svgToConvert = svgPath; +let tempNormalizedPath = null; + +if (!skipNormalize) { + const tempDir = path.join(__dirname, '.temp'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + tempNormalizedPath = path.join(tempDir, `normalized-${Date.now()}.svg`); + + try { + svgToConvert = normalizeSvg(svgPath, tempNormalizedPath); + } catch (error) { + console.error('💡 Try running with --skip-normalize if normalization causes issues'); + process.exit(1); + } +} else { + console.log('⏭️ Skipping SVG normalization (--skip-normalize flag set)'); +} + +const didNormalize = !skipNormalize; +convertSvgToComponent(svgToConvert, componentName, outputPath, config.defaultSize, didNormalize); + +if (tempNormalizedPath && fs.existsSync(tempNormalizedPath)) { + try { + fs.unlinkSync(tempNormalizedPath); + } catch (error) { + console.warn('⚠️ Failed to clean up temp SVGR config:', error.message); + } +} + generateAssetTypes(config); regenerateAssetRegistry(config);