diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 517035a4b..8ba58d114 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,13 +434,13 @@ importers: version: 9.39.5(jiti@2.7.0) jest: specifier: ^30.4.2 - version: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + version: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) rimraf: specifier: ^6.1.3 version: 6.1.3 ts-jest: specifier: ^29.4.12 - version: 29.4.12(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.0))(esbuild@0.27.7)(jest-util@30.4.1)(jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.12(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.0))(esbuild@0.28.2)(jest-util@30.4.1)(jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3) tsup: specifier: ^8.5.1 version: 8.5.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.13)(typescript@5.9.3) @@ -930,6 +930,15 @@ importers: scripts/sync-figma: dependencies: + '@svgr/core': + specifier: ^8.1.0 + version: 8.1.0(typescript@5.9.3) + '@svgr/plugin-jsx': + specifier: ^8.1.0 + version: 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + '@svgr/plugin-svgo': + specifier: ^8.1.0 + version: 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) lodash-es: specifier: ^4.18.1 version: 4.18.1 @@ -1062,10 +1071,6 @@ packages: resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.5': - resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} - engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} @@ -9856,26 +9861,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@7.28.5': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.28.5 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) - '@babel/helpers': 7.28.4 - '@babel/parser': 7.29.3 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -10036,15 +10021,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.5 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -12357,7 +12333,7 @@ snapshots: jest-util: 30.4.1 slash: 3.0.0 - '@jest/core@30.4.2(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3))': + '@jest/core@30.4.2(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3))': dependencies: '@jest/console': 30.4.1 '@jest/pattern': 30.4.0 @@ -12373,7 +12349,7 @@ snapshots: fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 jest-changed-files: 30.4.1 - jest-config: 30.4.2(@types/node@22.20.1)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + jest-config: 30.4.2(@types/node@22.20.1)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) jest-haste-map: 30.4.1 jest-message-util: 30.4.1 jest-regex-util: 30.4.0 @@ -13622,54 +13598,54 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.5)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.29.0 - '@svgr/babel-preset@8.1.0(@babel/core@7.28.5)': + '@svgr/babel-preset@8.1.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.5) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.5) + '@babel/core': 7.29.0 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.0) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.0) '@svgr/core@8.1.0(typescript@5.9.3)': dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) + '@babel/core': 7.29.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) camelcase: 6.3.0 cosmiconfig: 8.3.6(typescript@5.9.3) snake-case: 3.0.4 @@ -13679,13 +13655,13 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 entities: 4.5.0 '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) + '@babel/core': 7.29.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 @@ -15410,7 +15386,7 @@ snapshots: cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.3.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -15790,10 +15766,10 @@ snapshots: transitivePeerDependencies: - supports-color - esbuild-register@3.6.0(esbuild@0.27.7): + esbuild-register@3.6.0(esbuild@0.28.2): dependencies: debug: 4.4.3 - esbuild: 0.27.7 + esbuild: 0.28.2 transitivePeerDependencies: - supports-color optional: true @@ -17122,15 +17098,15 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): + jest-cli@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): dependencies: - '@jest/core': 30.4.2(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + '@jest/core': 30.4.2(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) '@jest/test-result': 30.4.1 '@jest/types': 30.4.1 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + jest-config: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) jest-util: 30.4.1 jest-validate: 30.4.1 yargs: 17.7.2 @@ -17141,7 +17117,7 @@ snapshots: - supports-color - ts-node - jest-config@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): + jest-config@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 @@ -17168,13 +17144,13 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.19.43 - esbuild-register: 3.6.0(esbuild@0.27.7) + esbuild-register: 3.6.0(esbuild@0.28.2) ts-node: 10.9.2(@types/node@20.19.43)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@30.4.2(@types/node@22.20.1)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): + jest-config@30.4.2(@types/node@22.20.1)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 @@ -17201,7 +17177,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.20.1 - esbuild-register: 3.6.0(esbuild@0.27.7) + esbuild-register: 3.6.0(esbuild@0.28.2) ts-node: 10.9.2(@types/node@20.19.43)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros @@ -17460,12 +17436,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): + jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)): dependencies: - '@jest/core': 30.4.2(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + '@jest/core': 30.4.2(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) '@jest/types': 30.4.1 import-local: 3.2.0 - jest-cli: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + jest-cli: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -19820,12 +19796,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.12(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.0))(esbuild@0.27.7)(jest-util@30.4.1)(jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.12(@babel/core@7.29.0)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.0))(esbuild@0.28.2)(jest-util@30.4.1)(jest@30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.9 - jest: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.7))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) + jest: 30.4.2(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.28.2))(ts-node@10.9.2(@types/node@20.19.43)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -19838,7 +19814,7 @@ snapshots: '@jest/transform': 30.4.1 '@jest/types': 30.4.1 babel-jest: 30.4.1(@babel/core@7.29.0) - esbuild: 0.27.7 + esbuild: 0.28.2 jest-util: 30.4.1 ts-morph@22.0.0: diff --git a/scripts/sync-figma/commands/sync-icons.ts b/scripts/sync-figma/commands/sync-icons.ts index b698d9155..279bd0c10 100644 --- a/scripts/sync-figma/commands/sync-icons.ts +++ b/scripts/sync-figma/commands/sync-icons.ts @@ -27,11 +27,14 @@ import { FIGMA_NODE_TYPES, } from '~/icons/constants'; import { ICON_TYPES } from '~/icons/icon-types'; -import getIconComponent from '~/icons/templates/icon/icon-component'; import getIconComponentIndex from '~/icons/templates/icon/icon-component-index'; import getIconsIndex from '~/icons/templates/icon/icons-index'; import type { IconNode } from '~/integrations/figma/lib'; -import { filterDocumentByNodeType, getIconJsx, getNodesWithUrl } from '~/integrations/figma/lib'; +import { + filterDocumentByNodeType, + getIconComponent, + getNodesWithUrl, +} from '~/integrations/figma/lib'; const TYPE = process.env.TYPE; @@ -44,12 +47,11 @@ function findRoot(dir: string): string { } const CURRENT_DIRECTORY = findRoot(path.dirname(fileURLToPath(import.meta.url))); const FIGMA_EMOJI_PREFIX_PATTERN = /❤️\s*/g; +// Resolve the repo's prettier config so generated files match what `pnpm format` produces, +// plugins (import sorting) included. const PRETTIER_OPTIONS = { + ...(await prettier.resolveConfig(path.join(CURRENT_DIRECTORY, 'packages/icons/src/index.ts'))), parser: 'typescript', - tabWidth: 4, - semi: true, - singleQuote: true, - printWidth: 100, }; function normalizeIconName(name: string) { @@ -138,8 +140,7 @@ try { parentId === decodeURIComponent(FIGMA_ICONS_SYMBOL_COLOR_NODE_ID) || parentId === decodeURIComponent(FIGMA_ICONS_SYMBOL_COLOR_COUNTRY_NODE_ID); - const iconJsx = await getIconJsx({ url, isColorIcon }); - const IconComponent = getIconComponent(iconName, iconJsx); + const IconComponent = await getIconComponent({ url, iconName, isColorIcon }); const formattedComponent = await prettier.format(IconComponent, PRETTIER_OPTIONS); let shouldWrite = isNewIcon; diff --git a/scripts/sync-figma/package.json b/scripts/sync-figma/package.json index 56bd76392..3ff124a2a 100644 --- a/scripts/sync-figma/package.json +++ b/scripts/sync-figma/package.json @@ -5,6 +5,8 @@ "description": "Internal CLI scripts for syncing assets from Figma", "type": "module", "scripts": { + "format": "prettier --write \"./{commands,src}/**/*.{ts,md}\"", + "format:check": "prettier --check \"./{commands,src}/**/*.{ts,md}\"", "lint": "eslint ./commands ./src", "notify:slack": "tsx ./commands/notify-slack.ts", "sync-icons:basic": "TYPE=basic tsx --env-file-if-exists=.env ./commands/sync-icons.ts", @@ -12,6 +14,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0", + "@svgr/plugin-svgo": "^8.1.0", "lodash-es": "^4.18.1", "p-limit": "^6.2.0", "picocolors": "^1.1.1", diff --git a/scripts/sync-figma/src/icons/templates/icon/icon-component.ts b/scripts/sync-figma/src/icons/templates/icon/icon-component.ts deleted file mode 100644 index 778a4e6d9..000000000 --- a/scripts/sync-figma/src/icons/templates/icon/icon-component.ts +++ /dev/null @@ -1,11 +0,0 @@ -export default (IconName: string, Icon: string) => ` - -import IconBase from '~/components/icon-base'; -import type { IconProps } from '~/components/icon-base'; - -const ${IconName} = (props : IconProps) => ( - ${Icon} -); - -export default ${IconName}; -`; diff --git a/scripts/sync-figma/src/integrations/figma/lib.ts b/scripts/sync-figma/src/integrations/figma/lib.ts index f7df33e81..9c5a65918 100644 --- a/scripts/sync-figma/src/integrations/figma/lib.ts +++ b/scripts/sync-figma/src/integrations/figma/lib.ts @@ -2,7 +2,7 @@ import type { FigmaNodeType } from '~/icons/constants'; import type { FigmaNode } from './api'; import { getFileNodes, getImage } from './api'; -import { makeFlexibleColorIcon, remakeMaskStyle, svgToIconComponent } from './transforms'; +import { svgToIconComponent } from './svgr'; type IconNode = FigmaNode & { parentId: string }; type IconNodeWithUrl = IconNode & { url: string }; @@ -79,30 +79,26 @@ const getNodesWithUrl = async ({ }; /** - * Convert svg files from Figma to React components. + * Convert an svg file from Figma into a React component. */ -const getIconJsx = async ({ +const getIconComponent = async ({ url, + iconName, isColorIcon, }: { url: string; + iconName: string; isColorIcon: boolean; }): Promise => { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch SVG: ${response.status} ${response.statusText}`); } - const svgDom = await response.text(); - const IconComponent = svgToIconComponent(svgDom); - const NewIconComponent = remakeMaskStyle(IconComponent); + const svg = await response.text(); - if (isColorIcon) { - return NewIconComponent; - } else { - return makeFlexibleColorIcon(NewIconComponent); - } + return svgToIconComponent({ svg, iconName, isColorIcon }); }; export type { IconNode, IconNodeWithUrl }; -export { filterDocumentByNodeType, getNodesWithUrl, getIconJsx }; +export { filterDocumentByNodeType, getNodesWithUrl, getIconComponent }; diff --git a/scripts/sync-figma/src/integrations/figma/svgr.ts b/scripts/sync-figma/src/integrations/figma/svgr.ts new file mode 100644 index 000000000..a60fd8add --- /dev/null +++ b/scripts/sync-figma/src/integrations/figma/svgr.ts @@ -0,0 +1,101 @@ +import type { Config } from '@svgr/core'; +import { transform } from '@svgr/core'; + +type SvgoPlugin = Extract< + NonNullable['plugins']>[number], + { fn: unknown } +>; + +/** + * Wrap the SVG in IconBase instead of rendering a bare . + */ +const template: Config['template'] = ({ componentName, jsx }, { tpl }) => { + const identifier = { + type: 'JSXIdentifier', + name: 'IconBase', + } as typeof jsx.openingElement.name; + jsx.openingElement.name = identifier; + if (jsx.closingElement) jsx.closingElement.name = identifier; + jsx.openingElement.attributes.push({ + type: 'JSXSpreadAttribute', + argument: { type: 'Identifier', name: 'props' }, + } as (typeof jsx.openingElement.attributes)[number]); + + return tpl` +import IconBase from '~/components/icon-base'; +import type { IconProps } from '~/components/icon-base'; + +const ${componentName} = (props: IconProps) => ( + ${jsx} +); + +export default ${componentName}; +`; +}; + +const BLACK = /^(#000|#000000|black)$/i; + +/** + * Figma paints mono icons with a literal black fill (and stroke on a few), which would ignore the + * colour IconBase hands down. Point both at currentColor; the root fill="none" Figma always emits + * stays put so stroked shapes keep inheriting `none` and stay hollow. + */ +const blackFollowsCurrentColor: SvgoPlugin = { + name: 'blackFollowsCurrentColor', + fn: () => ({ + element: { + enter: (node) => { + for (const attr of ['fill', 'stroke'] as const) { + if (BLACK.test(node.attributes[attr] ?? '')) + node.attributes[attr] = 'currentColor'; + } + }, + }, + }), +}; + +/** + * Build the SVGR config for a single icon. + * + * Passing svgoConfig replaces SVGR's default svgo config wholesale, so prefixIds has to be + * listed explicitly. Without it every icon's ids collapse to `a`, and two icons on one page + * make the second reference the first one's mask. + */ +const buildConfig = ({ + iconName, + isColorIcon, +}: { + iconName: string; + isColorIcon: boolean; +}): Config => ({ + typescript: true, + template, + plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], + svgoConfig: { + multipass: true, + plugins: [ + // removeViewBox would drop the viewBox because it matches width/height; + // removeDimensions then drops width/height instead, leaving IconBase in charge of size. + { name: 'preset-default', params: { overrides: { removeViewBox: false } } }, + 'removeDimensions', + { + name: 'prefixIds', + params: { prefix: `vapor-icons-${isColorIcon ? 'color' : 'mono'}-${iconName}` }, + }, + // Mono icons follow the consumer's `color`; colour icons keep Figma's palette. + ...(isColorIcon ? [] : [blackFollowsCurrentColor]), + ], + }, +}); + +const svgToIconComponent = ({ + svg, + iconName, + isColorIcon, +}: { + svg: string; + iconName: string; + isColorIcon: boolean; +}) => transform(svg, buildConfig({ iconName, isColorIcon }), { componentName: iconName }); + +export { svgToIconComponent }; diff --git a/scripts/sync-figma/src/integrations/figma/transforms.ts b/scripts/sync-figma/src/integrations/figma/transforms.ts deleted file mode 100644 index 14dc06483..000000000 --- a/scripts/sync-figma/src/integrations/figma/transforms.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { camelCase } from 'lodash-es'; - -const svgToIconComponent = (svgDom: string) => - svgDom - .toString() - .replace(/(?<=]*)\s+(width|height)="[^"]*"/g, '') - .replace(/^/gm, '') - .replace(/\s\w*:\w*="/gm, (w) => ' ' + camelCase(w) + '="') - .replace(/\s\w*-\w*="/gm, (w) => ' ' + camelCase(w) + '="') - .replace(/>/, ' {...props} >'); - -const remakeMaskStyle = (IconComponent: string) => { - const matches = IconComponent.matchAll(/(style)="(.*?)"/gi); - for (const match of matches) { - const splitColon = match[2].split(':'); - const splitHypen = splitColon[0].split('-'); - const key = splitHypen - .map((word, index) => { - if (index === 0) return word; - word = word.charAt(0).toUpperCase() + word.slice(1); - return word; - }) - .join(''); - const value = splitColon[1]; - const styleReplace = `${match[1]}={{ ${key}: "${value}" }}`; - IconComponent = IconComponent.replace(match[0], styleReplace); - } - return IconComponent; -}; - -const makeFlexibleColorIcon = (IconComponent: string) => - IconComponent.replace(/fill="black"/g, '').replace(/fill="none"/g, ''); - -export { svgToIconComponent, remakeMaskStyle, makeFlexibleColorIcon };