diff --git a/.changeset/fuzzy-camels-sip.md b/.changeset/fuzzy-camels-sip.md new file mode 100644 index 0000000..3c78a45 --- /dev/null +++ b/.changeset/fuzzy-camels-sip.md @@ -0,0 +1,5 @@ +--- +'@neodx/svg': minor +--- + +Introduce new `metadata` API diff --git a/README.md b/README.md index e1c2edf..53d7ddf 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ We have a some ideas for future development, so stay tuned and feel free to requ ### [@neodx/svg](./libs/svg) -Are you converting every SVG icon to React component with SVGR or something similar? It's so ease to use! +Are you converting every SVG icon to a React component with SVGR or something similar? It's so ease to use! -But wait, did you know that SVG sprites are native approach for icons? It's even easier to use! +But wait; did you know that SVG sprites are a native approach for icons? It's even easier to use! ```typescript jsx import { Icon } from '@/shared/ui'; @@ -75,7 +75,7 @@ export default defineConfig({ svg({ root: 'assets', output: 'public', - definitions: 'src/shared/ui/icon/sprite.h.ts' + metadata: 'src/shared/ui/icon/sprite.gen.ts' }) ] }); @@ -94,7 +94,7 @@ export default { svg({ root: 'assets', output: 'public', - definitions: 'src/shared/ui/icon/sprite.h.ts' + metadata: 'src/shared/ui/icon/sprite.gen.ts' }) ] }; @@ -113,7 +113,7 @@ export default { svg({ root: 'assets', output: 'public', - definitions: 'src/shared/ui/icon/sprite.h.ts' + metadata: 'src/shared/ui/icon/sprite.gen.ts' }) ] }; @@ -132,7 +132,7 @@ export default { svg({ root: 'assets', output: 'public', - definitions: 'src/shared/ui/icon/sprite.h.ts' + metadata: 'src/shared/ui/icon/sprite.gen.ts' }) ] }; @@ -144,7 +144,7 @@ export default { CLI ```shell -npx @neodx/svg --group --root assets --output public --definition src/shared/ui/icon/sprite.h.ts +npx @neodx/svg --group --root assets --output public --definition src/shared/ui/icon/sprite.gen.ts # --root - root folder with SVGs # --group - group icons by folders (assets/common/add.svg -> common/add, assets/other/cut.svg -> other/cut) # --output (-o) - output folder for sprites @@ -165,7 +165,7 @@ await buildSprites({ root: 'assets', input: '**/*.svg', output: 'public', - definition: 'src/shared/ui/icon/sprite.h.ts' + metadata: 'src/shared/ui/icon/sprite.gen.ts' }); ``` @@ -185,7 +185,7 @@ src/ shared/ ui/ icon/ -+ sprite.h.ts // sprite definitions - types, metadata, etc. ++ sprite.gen.ts // sprite definitions - types, metadata, etc. public/ + sprite/ + common.svg @@ -208,7 +208,7 @@ Having trouble finding a suitable logging library because they're too heavy, pla I faced the same issues, which led me to create `@neodx/log`. It's simple, efficient, and avoids most critical drawbacks. -Furthermore, it's easily replaceable and extensible, making it the great fit for your development needs. +Furthermore, it's easily replaceable and extensible, making it a great fit for your development needs.
Header diff --git a/apps/docs/svg/frameworks-and-bundlers.md b/apps/docs/svg/frameworks-and-bundlers.md index d98efb3..48c0d25 100644 --- a/apps/docs/svg/frameworks-and-bundlers.md +++ b/apps/docs/svg/frameworks-and-bundlers.md @@ -18,7 +18,7 @@ export default defineConfig({ root: 'assets', group: true, output: 'public', - definitions: 'src/shared/ui/icon/sprite.h.ts', + metadata: 'src/shared/ui/icon/sprite.gen.ts', resetColors: { replaceUnknown: 'currentColor' } diff --git a/examples/svg-magic-with-figma-export/src/shared/ui/icon/icon.tsx b/examples/svg-magic-with-figma-export/src/shared/ui/icon/icon.tsx index 910b6ad..a456d71 100644 --- a/examples/svg-magic-with-figma-export/src/shared/ui/icon/icon.tsx +++ b/examples/svg-magic-with-figma-export/src/shared/ui/icon/icon.tsx @@ -21,7 +21,7 @@ export function Icon({ name, className, viewBox, ...props }: IconProps) { aria-hidden {...props} > - + ); } diff --git a/examples/svg-magic-with-figma-export/vite.config.ts b/examples/svg-magic-with-figma-export/vite.config.ts index ca9481b..bc084c2 100644 --- a/examples/svg-magic-with-figma-export/vite.config.ts +++ b/examples/svg-magic-with-figma-export/vite.config.ts @@ -14,10 +14,10 @@ export default defineConfig({ root: 'assets/icons', output: 'public', group: true, + metadata: 'src/shared/ui/icon/sprite.gen.ts', resetColors: { replace: ['#6C707E', '#A8ADBD', '#818594'] - }, - definitions: 'src/shared/ui/icon/sprite.gen.ts' + } }) ] }); diff --git a/examples/svg-next/next.config.mjs b/examples/svg-next/next.config.mjs index e4d31dd..f27a263 100644 --- a/examples/svg-next/next.config.mjs +++ b/examples/svg-next/next.config.mjs @@ -10,7 +10,7 @@ const nextConfig = { svg({ root: 'assets', output: 'public', - definitions: 'src/shared/ui/icon/sprite.gen.ts', + metadata: 'src/shared/ui/icon/sprite.gen.ts', resetColors: { replaceUnknown: 'currentColor' } diff --git a/examples/svg-next/src/shared/ui/icon/icon.tsx b/examples/svg-next/src/shared/ui/icon/icon.tsx index 60de46e..3f3e73f 100644 --- a/examples/svg-next/src/shared/ui/icon/icon.tsx +++ b/examples/svg-next/src/shared/ui/icon/icon.tsx @@ -17,7 +17,7 @@ export function Icon({ name, className, viewBox, ...props }: IconProps) { aria-hidden {...props} > - + ); } diff --git a/examples/svg-vite/README.md b/examples/svg-vite/README.md index d0a61cf..ea66613 100644 --- a/examples/svg-vite/README.md +++ b/examples/svg-vite/README.md @@ -1,10 +1,9 @@ # Example of using `@neodx/svg` Vite plugin -> **Warning** In this example was used `experimentalRuntime` option and advanced `fileName` feature, API will be changed in the nearest future. - This example shows how to use `@neodx/svg` as Vite plugin and simple step-by-step setup for React. -In the addition you can see how to use multicolored icons with TailwindCSS and CSS variable (it's not very pleasant, but it works 🌝). +In addition, you can see how to use multicolored icons with TailwindCSS and CSS variable +(it's not very pleasant, but it works 🌝). ![result](./docs/result.png) @@ -37,7 +36,14 @@ export default defineConfig(({ command }) => ({ root: 'assets', // Root folder for SVG files, all source paths will be relative to this folder group: true, // Group SVG files by folder output: 'public', // Output folder for generated files - definitions: 'src/shared/ui/icon/sprite.gen.ts', // Output file for generated TypeScript definitions + metadata: { + path: 'src/shared/ui/icon/sprite.gen.ts', // Output file for generated TypeScript definitions + runtime: { + // Generate additional runtime information + size: true, + viewBox: true + } + }, resetColors: { replace: ['#000', '#eee', '#6C707E'], // Resets all known colors to `currentColor` replaceUnknown: 'var(--icon-color)' // Replaces unknown colors with custom CSS variable @@ -47,14 +53,14 @@ export default defineConfig(({ command }) => ({ })); ``` -## Create Icon component and describe basic styles +## Create an Icon component and describe basic styles [shared/ui/icon/icon.tsx](./src/shared/ui/icon/icon.tsx): ```tsx import clsx from 'clsx'; import type { SVGProps } from 'react'; -import type { SpritesMap } from './sprite.gen'; +import { SPRITES_META, type SpritesMap } from './sprite.gen'; // Merging all icons as `SPRITE_NAME/ICON_NAME` export type SpriteKey = { @@ -66,18 +72,30 @@ export interface IconProps extends Omit, 'name' | 'type' } export function Icon({ name, className, viewBox, ...props }: IconProps) { - const [spriteName, iconName] = name.split('/'); + const [spriteName, iconName] = name.split('/') as [ + keyof SpritesMap, + SpritesMap[keyof SpritesMap] + ]; + const { filePath, items } = SPRITES_META[spriteName]; + // @ts-expect-error mixed structures are confusing TS + const { viewBox, width, height } = items[iconName]; + const rect = width === height ? 'xy' : width > height ? 'x' : 'y'; return ( - + ); } @@ -100,7 +118,19 @@ export function Icon({ name, className, viewBox, ...props }: IconProps) { @layer components { /* Our base class for all icons */ .icon { - @apply select-none fill-current w-[1em] h-[1em] inline-block text-inherit box-content; + @apply select-none fill-current inline-block text-inherit box-content; + } + + .icon[data-icon-aspect-ratio='xy'] { + @apply w-[1em] h-[1em]; + } + + .icon[data-icon-aspect-ratio='x'] { + @apply w-[1em]; + } + + .icon[data-icon-aspect-ratio='y'] { + @apply h-[1em]; } } ``` @@ -125,8 +155,8 @@ Under this example I want to cover all planned features of `@neodx/svg`, you can - [x] Colors: replace known to CSS variables - [x] Colors: exclude specific icons - [x] Colors: exclude specific colors -- [ ] Non-standard sizes: generate `viewBox` and `width`/`height` attributes -- [ ] Non-standard sizes: example of enhanced `Icon` component +- [x] Non-standard sizes: generate `viewBox` and `width`/`height` attributes +- [x] Non-standard sizes: example of enhanced `Icon` component - [ ] Inline SVG: auto-detection of internal references - [ ] Inline SVG: injection into HTML - [ ] Remove unnecessary attributes diff --git a/examples/svg-vite/src/shared/ui/icon/icon.tsx b/examples/svg-vite/src/shared/ui/icon/icon.tsx index 3169351..3012a30 100644 --- a/examples/svg-vite/src/shared/ui/icon/icon.tsx +++ b/examples/svg-vite/src/shared/ui/icon/icon.tsx @@ -10,24 +10,30 @@ export interface IconProps extends Omit, 'name' | 'type' name: IconName; } -export function Icon({ name, className, viewBox: viewBoxFromProps, ...props }: IconProps) { +export function Icon({ name, className, ...props }: IconProps) { const [spriteName, iconName] = name.split('/') as [ keyof SpritesMap, SpritesMap[keyof SpritesMap] ]; const { filePath, items } = SPRITES_META[spriteName]; // TODO Fix types - const { viewBox } = (items as any)[iconName] as { viewBox: string }; + const { viewBox, width, height } = (items as any)[iconName] as any; + const rect = width === height ? 'xy' : width > height ? 'x' : 'y'; return ( - + ); } diff --git a/examples/svg-vite/src/shared/ui/icon/sprite.gen.ts b/examples/svg-vite/src/shared/ui/icon/sprite.gen.ts index e4ceefa..ecb13b0 100644 --- a/examples/svg-vite/src/shared/ui/icon/sprite.gen.ts +++ b/examples/svg-vite/src/shared/ui/icon/sprite.gen.ts @@ -425,115 +425,184 @@ export interface SpritesMap { | 'zw'; logos: 'linkedin' | 'twitter'; } - export const SPRITES_META = { common: { filePath: 'common.76103a6c.svg', items: { add: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'autoscroll-from-source': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'autoscroll-to-source': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, checkmark: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'chevron-down-large': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'chevron-down': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'chevron-left': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'chevron-right': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'chevron-up-large': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'chevron-up': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'close-small-hovered': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'close-small': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, close: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'collapse-all': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, copy: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, cut: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, delete: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'double-color': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, down: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, download: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, edit: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, exit: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'expand-all': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, export: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'external-link': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, favourite: { - viewBox: '0 0 48 48' + viewBox: '0 0 48 48', + width: 48, + height: 48 }, filter: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, groups: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, help: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, hide: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, history: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, 'ide-update': { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, import: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, layout: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 }, left: { - viewBox: '0 0 16 16' + viewBox: '0 0 16 16', + width: 16, + height: 16 } } }, @@ -541,1165 +610,1939 @@ export const SPRITES_META = { filePath: 'flags.49fa86bc.svg', items: { ac: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ad: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ae: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'af-emirate': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, af: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, afar: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ag: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ai: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, al: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, am: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, an: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ao: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'aq-true_south': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, aq: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ar: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, as: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, at: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'au-aboriginal': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'au-act': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'au-nsw': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'au-nt': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'au-qld': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'au-sa': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'au-tas': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'au-vic': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'au-wa': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, au: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, aw: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ax: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, az: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ba: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bb: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bd: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, be: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bf: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bh: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bi: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bj: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bl: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bn: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bo: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'bq-bo': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'bq-sa': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'bq-se': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bq: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, br: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bs: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bt: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bv: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bw: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, by: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, bz: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ca-bc': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ca-qc': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ca: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cc: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cd: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cf: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ch-gr': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ch: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ci: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ck: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cl: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'cn-hk': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'cn-xj': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cn: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, co: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cp: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cq: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cu: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cv: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cw: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cx: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cy: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, cz: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, de: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, dg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, dj: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, dk: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, dm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, do: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, dz: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ea: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, earth: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, east_african_federation: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, easter_island: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ec-w': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ec: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ee: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, eg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, eh: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, er: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'es-ar': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'es-ce': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'es-cn': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'es-ct': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'es-ga': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'es-ib': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'es-ml': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'es-pv': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'es-variant': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, es: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'et-or': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'et-ti': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, et: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, eu: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, european_union: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ewe: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, fi: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, fj: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, fk: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, fm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, fo: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'fr-20r': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'fr-bre': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'fr-cp': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, fr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, fx: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ga: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'gb-con': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'gb-eng': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'gb-nir': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'gb-ork': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'gb-sct': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'gb-wls': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gb: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gd: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ge-ab': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ge: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gf: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gh: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gi: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gl: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gn: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gp: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gq: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gs: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gt: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gu: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, guarani: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gw: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, gy: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, hausa: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, hk: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, hm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, hmong: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, hn: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, hr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ht: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, hu: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ic: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'id-jb': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'id-jt': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, id: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ie: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, il: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, im: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'in-as': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'in-gj': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'in-ka': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'in-mn': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'in-mz': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'in-or': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'in-tg': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'in-tn': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, in: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, io: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'iq-kr': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, iq: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ir: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, is: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'it-23': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'it-82': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'it-88': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, it: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, je: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, jm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, jo: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, jp: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, kanuri: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ke: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, kg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, kh: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ki: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, kikuyu: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, km: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, kn: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, kongo: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, kp: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, kr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, kw: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ky: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, kz: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, la: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, lb: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, lc: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, li: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, lk: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, lr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ls: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, lt: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, lu: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, lv: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ly: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ma: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, malayali: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, maori: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mars: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mc: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, md: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, me: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mf: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mh: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mk: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ml: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mn: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mo: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mp: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'mq-old': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mq: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ms: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mt: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mu: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mv: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mw: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mx: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, my: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, mz: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, na: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, nato: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, nc: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ne: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, nf: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ng: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ni: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'nl-fr': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, nl: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, no: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, northern_cyprus: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, np: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, nr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, nu: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, nz: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, occitania: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, olympics: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, om: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, otomi: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pa: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pe: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pf: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ph: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'pk-jk': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'pk-sd': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pk: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pl: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pn: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ps: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'pt-20': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'pt-30': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pt: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, pw: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, py: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, qa: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, quechua: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, re: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ro: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, rs: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ru-ba': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ru-ce': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ru-cu': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ru-da': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ru-dpr': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ru-ko': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ru-lpr': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ru-ta': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'ru-ud': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ru: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, rw: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sa: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sami: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sb: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sc: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sd: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, se: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'sh-ac': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'sh-hl': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'sh-ta': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sh: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, si: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sj: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sk: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sl: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sn: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, so: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, somaliland: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, south_ossetia: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, soviet_union: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ss: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, st: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, su: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sv: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sx: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sy: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, sz: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ta: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tc: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, td: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tf: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, th: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tibet: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tj: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tk: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tl: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tn: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, to: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, torres_strait_islands: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tr: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, transnistria: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tt: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tv: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tw: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, tz: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ua: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ug: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, uk: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, um: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, un: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, united_nations: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-ak': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-al': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-ar': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-az': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-ca': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-co': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-dc': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-fl': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-ga': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-hi': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-in': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-mo': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-ms': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-nc': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-nm': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-ri': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-tn': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, 'us-tx': { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, us: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, uy: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, uz: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, va: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, vc: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ve: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, vg: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, vi: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, vn: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, vu: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, wf: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, wiphala: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ws: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, xk: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, xx: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, ye: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, yorubaland: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, yt: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, yu: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, za: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, zm: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 }, zw: { - viewBox: '0 0 512 512' + viewBox: '0 0 512 512', + width: 512, + height: 512 } } }, @@ -1707,20 +2550,28 @@ export const SPRITES_META = { filePath: 'logos.bab17578.svg', items: { linkedin: { - viewBox: '0 0 140 34' + viewBox: '0 0 140 34', + width: 140, + height: 34 }, twitter: { - viewBox: '0 0 248 204' + viewBox: '0 0 248 204', + width: 248, + height: 204 } } } -} satisfies { - [SpriteName in keyof SpritesMap]: { +} satisfies Record< + string, + { filePath: string; - items: { - [ItemName in SpritesMap[SpriteName]]: { + items: Record< + string, + { viewBox: string; - }; - }; - }; -}; + width: number; + height: number; + } + >; + } +>; diff --git a/examples/svg-vite/src/shared/ui/index.css b/examples/svg-vite/src/shared/ui/index.css index c9aff48..9518485 100644 --- a/examples/svg-vite/src/shared/ui/index.css +++ b/examples/svg-vite/src/shared/ui/index.css @@ -10,6 +10,18 @@ @layer components { .icon { - @apply select-none fill-current w-[1em] h-[1em] inline-block text-inherit box-content; + @apply select-none fill-current inline-block text-inherit box-content; + } + + .icon[data-icon-aspect-ratio='xy'] { + @apply w-[1em] h-[1em]; + } + + .icon[data-icon-aspect-ratio='x'] { + @apply w-[1em]; + } + + .icon[data-icon-aspect-ratio='y'] { + @apply h-[1em]; } } diff --git a/examples/svg-vite/vite.config.ts b/examples/svg-vite/vite.config.ts index e771391..be62801 100644 --- a/examples/svg-vite/vite.config.ts +++ b/examples/svg-vite/vite.config.ts @@ -12,8 +12,13 @@ export default defineConfig({ group: true, output: 'public', fileName: '{name}.{hash:8}.svg', - experimentalRuntime: true, - definitions: 'src/shared/ui/icon/sprite.gen.ts', + metadata: { + path: 'src/shared/ui/icon/sprite.gen.ts', + runtime: { + size: true, + viewBox: true + } + }, resetColors: { exclude: [/^flags/, /^logos/], replace: ['#000', '#eee', '#6C707E', '#313547'], diff --git a/libs/svg/README.md b/libs/svg/README.md index 7af36a5..b7e641f 100644 --- a/libs/svg/README.md +++ b/libs/svg/README.md @@ -5,11 +5,11 @@ Supercharge your icons ⚡️ ## Motivation Sprites are the most effective way to work with your SVG icons, -but for some reason developers (vision from react world) prefer -mostly bloated and ineffective - "compile" SVG to react component with inlined SVG content. +but for some reason developers (vision from a React world) prefer +mostly bloated and ineffective, "compile" SVG to react component with inlined SVG content. Of course, we can use some external tools like https://svgsprit.es/ or some npm libraries, -but that's not serious (if you know any alternatives - let me know, and I'll add links), developers need DX. +but that's not serious (if you know any alternatives, let me know, and I'll add links), developers need DX. In a ridiculous, but incredibly popular way, we don't have other solutions with the same DX. @@ -23,11 +23,11 @@ is better than a super-efficient, but unusable setup with semi-manual generators That's why we're here! 🥳 -- TypeScript support out of box - generated types and [information about your sprites](#content-based-hashes) +- TypeScript support out of box - generated types and [information about your sprites](#-content-based-hashes-and-runtime-metadata-generation) - [Built-in integrated plugins](#integrate-with-your-bundler) for all major bundlers: `vite`, `webpack`, `rollup`, `esbuild`, etc. - Optional grouping by folders - Optimization with svgo -- [Automatically reset colors](#-powerful-colors-reset) +- [Automatically reset colors](#-automatically-reset-colors) - Powerful files selection ## Installation and usage @@ -43,7 +43,8 @@ pnpm add -D @neodx/svg ### CLI (Not recommended) -Currently, we don't recommend using CLI mode, because it's not flexible enough and requires extra setup, if you want to use it - see [CLI](#cli) section and [CLI Options API](#cli-options). +Currently, we don't recommend using CLI mode because it's not flexible enough and requires extra setup +if you want to use it - see [CLI](#cli) section and [CLI Options API](#cli-options). ```shell yarn sprite --help @@ -65,7 +66,7 @@ export default defineConfig({ root: 'assets', group: true, output: 'public', - definitions: 'src/shared/ui/icon/sprite.gen.ts', + metadata: 'src/shared/ui/icon/sprite.gen.ts', resetColors: { replaceUnknown: 'currentColor' } @@ -98,7 +99,7 @@ export default { svg({ root: 'assets', output: 'public', - definition: 'src/shared/ui/icon/sprite.gen.ts' + metadata: 'src/shared/ui/icon/sprite.gen.ts' }) ] }; @@ -117,7 +118,7 @@ export default { svg({ root: 'assets', output: 'public', - definition: 'src/shared/ui/icon/sprite.gen.ts' + metadata: 'src/shared/ui/icon/sprite.gen.ts' }) ] }; @@ -136,7 +137,7 @@ export default { svg({ root: 'assets', output: 'public', - definition: 'src/shared/ui/icon/sprite.gen.ts' + metadata: 'src/shared/ui/icon/sprite.gen.ts' }) ] }; @@ -276,9 +277,9 @@ svg({ }); ``` -### 🆕 ⚠️ Get content-based hashes in filenames with experimental runtime information +### 🆕 Content-based hashes and runtime metadata generation -> **Warning:** This feature is experimental and will be changed in the future. +> **Note:** If you used `definitions` or `experimentalRuntime` options before, you need to update your configuration, see [Migration guide](#move-from-definitions-and-experimentalruntime-options-to-metadata-api). By default, you will get the following sprites in your output: @@ -295,7 +296,7 @@ the sprite filename won't be updated, which could result in an infinite cache. To solve this issue and achieve content-based hashes in filenames, you need to take three steps: 1. Provide the `fileName` option with a `hash` variable (e.g. `fileName: "{name}.{hash:8}.svg"`) -2. Enable the `experimentalRuntime` option to get information about the file path by sprite name during runtime +2. Configure the `metadata` option to get additional information about the file path by sprite name during runtime 3. Update your `Icon` component (or whatever you use) to support the new runtime information ```typescript @@ -305,7 +306,13 @@ export default defineConfig({ plugins: [ svg({ fileName: '{name}.{hash:8}.svg', - experimentalRuntime: true + metadata: { + path: 'src/shared/ui/icon/sprite.gen.ts', + runtime: { + size: true, + viewBox: true + } + } // ... }) // ... @@ -322,6 +329,37 @@ public/ + sprite-bar.87654def.svg ``` +With the following metadata in `src/shared/ui/icon/sprite.gen.ts`: + +```typescript +export interface SpritesMap { + 'sprite-foo': 'first' | 'second'; + 'sprite-bar': ' /* ... */ '; +} +export const SPRITES_META = { + 'sprite-foo': { + filePath: 'sprite-foo.12abc678.svg', + items: { + first: { + // all items will have `viewBox`, `width` and `height` properties + viewBox: '0 0 48 48', + width: 48, + height: 48 + }, + second: { + /* ... */ + } + } + }, + 'sprite-bar': { + filePath: 'sprite-bar.87654def.svg', + items: { + /* ... */ + } + } +}; +``` + And updates of `Icon` component will be like this: > This example is based on implementation from [Building Icon component with TailwindCSS](#building-icon-component-with-tailwindcss-see-example) recipe and our [Vite application example (link to GH repo)](https://github.com/secundant/neodx/tree/main/examples/svg-vite) @@ -329,17 +367,17 @@ And updates of `Icon` component will be like this: ```diff + import { SPRITES_META, type SpritesMap } from './sprite.gen'; -+ export function Icon({ name, viewBox: viewBoxFromProps, /* ... */ }) { ++ export function Icon({ name, /* ... */ }) { const [spriteName, iconName] = name.split('/'); + const { filePath, items } = SPRITES_META[spriteName]; + const { viewBox } = items[iconName]; return ( -+ ++ ); } @@ -386,7 +424,9 @@ export default defineConfig({ root: 'assets', group: true, output: 'public', - definitions: 'src/shared/ui/icon/sprite.gen.ts', + metadata: { + path: 'src/shared/ui/icon/sprite.gen.ts' + }, resetColors: { replaceUnknown: 'currentColor' } @@ -445,11 +485,11 @@ export const SPRITES_META: { [K in keyof SpritesMap]: SpritesMap[K][] } = { As you can see, we have a map of all sprites and meta information about them. -Now we can use it in our code - for type checking, autocomplete and other cool stuff. +Now we can use it in our code - for type checking, autocomplete, and other cool stuff. ### Create your Icon component -> It's a **simple** implementation, you can see a more real one in the "Recipes" section +> It's a **simple** implementation, you can see a real one in the "Recipes" section ```tsx // shared/ui/icon/icon.tsx @@ -463,7 +503,7 @@ export interface IconProps { export function Icon({ type, name }: IconProps) { return ( - + ); } @@ -558,7 +598,7 @@ export function Icon({ name, className, viewBox, ...props }: IconProps) { {...props} > {/* For example, "/common.svg#favourite". Change base path if you don't store sprites under the root. */} - + ); } @@ -639,6 +679,27 @@ export function SomeFeature() { } ``` +## Migrations + +### Move from `definitions` and `experimentalRuntime` options to `metadata API` + +Now [metadata](#-content-based-hashes-and-runtime-metadata-generation) is stable +and covered under one `metadata` option. + +```diff +svg({ +- definitions: 'src/shared/ui/icon/sprite.gen.ts', +- experimentalRuntime: true, ++ metadata: { ++ path: 'src/shared/ui/icon/sprite.gen.ts', ++ runtime: { ++ size: true, ++ viewBox: true, ++ } ++ } +}); +``` + ## API ### Node.JS API @@ -696,36 +757,17 @@ interface Options { */ optimize?: boolean; /** - * Path to generated definitions file + * Configures metadata generation + * @example "src/sprites/meta.ts" + * @example { path: "meta.ts", runtime: false } // will generate only types + * @example { path: "meta.ts", types: 'TypeName', runtime: 'InfoName' } // will generate "interface TypeName" types and "const InfoName" runtime metadata + * @example { path: "meta.ts", runtime: { size: true, viewBox: true } } // will generate runtime metadata with size and viewBox */ - definitions?: string; + metadata?: MetadataPluginParams; /** * Reset colors config */ resetColors?: ResetColorsPluginParams; - /** - * WILL BE CHANGED IN FUTURE - * Replaces current approach (just array of IDs per sprite) with extended runtime metadata - * - * @unstable - * @example - * export const SPRITES_META = { - * 'common-arrows': { - * fileName: 'common/arrows.a766b3.svg', - * items: { - * left: { - * viewBox: '0 0 24 24', - * }, - * right: { - * viewBox: '0 0 24 24', - * }, - * // ... - * } - * }, - * // ... - * }; - */ - experimentalRuntime?: boolean; } ``` diff --git a/libs/svg/examples/colors-advanced/generated/sprite-info.ts b/libs/svg/examples/colors-advanced/generated/sprite-info.ts index 6a02592..725b78e 100644 --- a/libs/svg/examples/colors-advanced/generated/sprite-info.ts +++ b/libs/svg/examples/colors-advanced/generated/sprite-info.ts @@ -3,9 +3,12 @@ export interface SpritesMap { flags: 'ac' | 'ad' | 'ae' | 'af'; logos: 'linkedin' | 'twitter'; } - -export const SPRITES_META: { [K in keyof SpritesMap]: SpritesMap[K][] } = { +export const SPRITES_META = { sprite: ['close', 'exit', 'favourite', 'folder-colored', 'sort-by-visibility'], flags: ['ac', 'ad', 'ae', 'af'], logos: ['linkedin', 'twitter'] +} satisfies { + sprite: Array<'close' | 'exit' | 'favourite' | 'folder-colored' | 'sort-by-visibility'>; + flags: Array<'ac' | 'ad' | 'ae' | 'af'>; + logos: Array<'linkedin' | 'twitter'>; }; diff --git a/libs/svg/examples/colors/generated/sprite-info.ts b/libs/svg/examples/colors/generated/sprite-info.ts index 92ddc92..ac8d7d1 100644 --- a/libs/svg/examples/colors/generated/sprite-info.ts +++ b/libs/svg/examples/colors/generated/sprite-info.ts @@ -1,7 +1,8 @@ export interface SpritesMap { sprite: 'custom' | 'fill' | 'mixed' | 'stroke'; } - -export const SPRITES_META: { [K in keyof SpritesMap]: SpritesMap[K][] } = { +export const SPRITES_META = { sprite: ['custom', 'fill', 'mixed', 'stroke'] +} satisfies { + sprite: Array<'custom' | 'fill' | 'mixed' | 'stroke'>; }; diff --git a/libs/svg/examples/groups-with-root/generated/sprite-info.ts b/libs/svg/examples/groups-with-root/generated/sprite-info.ts index 1d07f76..cc530dd 100644 --- a/libs/svg/examples/groups-with-root/generated/sprite-info.ts +++ b/libs/svg/examples/groups-with-root/generated/sprite-info.ts @@ -2,8 +2,10 @@ export interface SpritesMap { common: 'close' | 'favourite'; format: 'align-left' | 'tag'; } - -export const SPRITES_META: { [K in keyof SpritesMap]: SpritesMap[K][] } = { +export const SPRITES_META = { common: ['close', 'favourite'], format: ['align-left', 'tag'] +} satisfies { + common: Array<'close' | 'favourite'>; + format: Array<'align-left' | 'tag'>; }; diff --git a/libs/svg/examples/groups-without-root/generated/sprite-info.ts b/libs/svg/examples/groups-without-root/generated/sprite-info.ts index 645c88a..dab91a7 100644 --- a/libs/svg/examples/groups-without-root/generated/sprite-info.ts +++ b/libs/svg/examples/groups-without-root/generated/sprite-info.ts @@ -2,8 +2,10 @@ export interface SpritesMap { 'assets/common': 'close' | 'favourite'; 'assets/format': 'align-left' | 'tag'; } - -export const SPRITES_META: { [K in keyof SpritesMap]: SpritesMap[K][] } = { +export const SPRITES_META = { 'assets/common': ['close', 'favourite'], 'assets/format': ['align-left', 'tag'] +} satisfies { + 'assets/common': Array<'close' | 'favourite'>; + 'assets/format': Array<'align-left' | 'tag'>; }; diff --git a/libs/svg/examples/experimental-runtime/assets/common/close.svg b/libs/svg/examples/metadata/assets/common/close.svg similarity index 100% rename from libs/svg/examples/experimental-runtime/assets/common/close.svg rename to libs/svg/examples/metadata/assets/common/close.svg diff --git a/libs/svg/examples/experimental-runtime/assets/common/favourite.svg b/libs/svg/examples/metadata/assets/common/favourite.svg similarity index 100% rename from libs/svg/examples/experimental-runtime/assets/common/favourite.svg rename to libs/svg/examples/metadata/assets/common/favourite.svg diff --git a/libs/svg/examples/experimental-runtime/assets/format/align-left.svg b/libs/svg/examples/metadata/assets/format/align-left.svg similarity index 100% rename from libs/svg/examples/experimental-runtime/assets/format/align-left.svg rename to libs/svg/examples/metadata/assets/format/align-left.svg diff --git a/libs/svg/examples/experimental-runtime/assets/format/tag.svg b/libs/svg/examples/metadata/assets/format/tag.svg similarity index 100% rename from libs/svg/examples/experimental-runtime/assets/format/tag.svg rename to libs/svg/examples/metadata/assets/format/tag.svg diff --git a/libs/svg/examples/experimental-runtime/generated/common.2eb4b56f.svg b/libs/svg/examples/metadata/generated/common.2eb4b56f.svg similarity index 100% rename from libs/svg/examples/experimental-runtime/generated/common.2eb4b56f.svg rename to libs/svg/examples/metadata/generated/common.2eb4b56f.svg diff --git a/libs/svg/examples/experimental-runtime/generated/format.c890959d.svg b/libs/svg/examples/metadata/generated/format.c890959d.svg similarity index 100% rename from libs/svg/examples/experimental-runtime/generated/format.c890959d.svg rename to libs/svg/examples/metadata/generated/format.c890959d.svg diff --git a/libs/svg/examples/experimental-runtime/generated/sprite-info.ts b/libs/svg/examples/metadata/generated/meta.ts similarity index 50% rename from libs/svg/examples/experimental-runtime/generated/sprite-info.ts rename to libs/svg/examples/metadata/generated/meta.ts index 46cbc3d..841f35d 100644 --- a/libs/svg/examples/experimental-runtime/generated/sprite-info.ts +++ b/libs/svg/examples/metadata/generated/meta.ts @@ -2,16 +2,19 @@ export interface SpritesMap { common: 'close' | 'favourite'; format: 'align-left' | 'tag'; } - export const SPRITES_META = { common: { filePath: 'common.2eb4b56f.svg', items: { close: { - viewBox: '0 0 48 48' + viewBox: '0 0 48 48', + width: 48, + height: 48 }, favourite: { - viewBox: '0 0 48 48' + viewBox: '0 0 48 48', + width: 48, + height: 48 } } }, @@ -19,20 +22,28 @@ export const SPRITES_META = { filePath: 'format.c890959d.svg', items: { 'align-left': { - viewBox: '0 0 48 48' + viewBox: '0 0 48 48', + width: 48, + height: 48 }, tag: { - viewBox: '0 0 48 48' + viewBox: '0 0 48 48', + width: 48, + height: 48 } } } -} satisfies { - [SpriteName in keyof SpritesMap]: { +} satisfies Record< + string, + { filePath: string; - items: { - [ItemName in SpritesMap[SpriteName]]: { + items: Record< + string, + { viewBox: string; - }; - }; - }; -}; + width: number; + height: number; + } + >; + } +>; diff --git a/libs/svg/examples/react/generated/sprite-info.ts b/libs/svg/examples/react/generated/sprite-info.ts index 1d07f76..cc530dd 100644 --- a/libs/svg/examples/react/generated/sprite-info.ts +++ b/libs/svg/examples/react/generated/sprite-info.ts @@ -2,8 +2,10 @@ export interface SpritesMap { common: 'close' | 'favourite'; format: 'align-left' | 'tag'; } - -export const SPRITES_META: { [K in keyof SpritesMap]: SpritesMap[K][] } = { +export const SPRITES_META = { common: ['close', 'favourite'], format: ['align-left', 'tag'] +} satisfies { + common: Array<'close' | 'favourite'>; + format: Array<'align-left' | 'tag'>; }; diff --git a/libs/svg/examples/react/icon.tsx b/libs/svg/examples/react/icon.tsx index cf57d17..26df2f3 100644 --- a/libs/svg/examples/react/icon.tsx +++ b/libs/svg/examples/react/icon.tsx @@ -24,7 +24,7 @@ export function Icon({ name, className, viewBox, ...props }: IconProps) { aria-hidden {...props} > - + ); } diff --git a/libs/svg/examples/simple/generated/sprite-info.ts b/libs/svg/examples/simple/generated/sprite-info.ts index 8d52b80..2a05ea8 100644 --- a/libs/svg/examples/simple/generated/sprite-info.ts +++ b/libs/svg/examples/simple/generated/sprite-info.ts @@ -1,7 +1,8 @@ export interface SpritesMap { sprite: 'arrow-drop-down' | 'arrow-drop-up'; } - -export const SPRITES_META: { [K in keyof SpritesMap]: SpritesMap[K][] } = { +export const SPRITES_META = { sprite: ['arrow-drop-down', 'arrow-drop-up'] +} satisfies { + sprite: Array<'arrow-drop-down' | 'arrow-drop-up'>; }; diff --git a/libs/svg/src/__tests__/__snapshots__/examples.test.ts.snap b/libs/svg/src/__tests__/__snapshots__/examples.test.ts.snap index efc7ae2..71d4b5f 100644 --- a/libs/svg/src/__tests__/__snapshots__/examples.test.ts.snap +++ b/libs/svg/src/__tests__/__snapshots__/examples.test.ts.snap @@ -19,7 +19,7 @@ exports[`examples > "colors" example should replay same output 1`] = ` ], [ "generated/sprite-info.ts", - "4793076202450a65b7b04efe27b0b74ddda29399e95da47f55cfe47450854d12", + "68ee54e6a56a9c10472f3613d7b688b49087d69ca2ad2591318a01138ff5f669", ], ] `; @@ -60,94 +60,94 @@ exports[`examples > "colors-advanced" example should replay same output 1`] = ` ], [ "generated/sprite-info.ts", - "ccea76ebfa7898989ef6da5c21ff18e8225a629e84098600b52debde57b37d44", + "666e8fa9a10d2e5127d31cb8f8604ab36c16aaba9485ceb31aeb18e87e5cddd0", ], ] `; -exports[`examples > "experimental-runtime" example should generate files 1`] = ` +exports[`examples > "groups-with-root" example should generate files 1`] = ` [ "assets/common/close.svg", "assets/common/favourite.svg", "assets/format/align-left.svg", "assets/format/tag.svg", - "generated/common.2eb4b56f.svg", - "generated/format.c890959d.svg", + "generated/common.svg", + "generated/format.svg", "generated/sprite-info.ts", ] `; -exports[`examples > "experimental-runtime" example should replay same output 1`] = ` +exports[`examples > "groups-with-root" example should replay same output 1`] = ` [ [ - "generated/common.2eb4b56f.svg", + "generated/common.svg", "2eb4b56ff266279ca9c0af3b16f7f3114ae35c4555657bb106acbd15ff9d4f52", ], [ - "generated/format.c890959d.svg", + "generated/format.svg", "c890959d268bf53d6006012438788b4104189d3cfaf997b0b43b2fd4f1a41159", ], [ "generated/sprite-info.ts", - "046df9f47812fc3681fd787ae23e64db1f089a509fd18607a57d2613bf4cdff7", + "0c82bd5eb1a18ea3337261408254d09db5cee2c8eeb4561ed6bca996b0e0d9c9", ], ] `; -exports[`examples > "groups-with-root" example should generate files 1`] = ` +exports[`examples > "groups-without-root" example should generate files 1`] = ` [ "assets/common/close.svg", "assets/common/favourite.svg", "assets/format/align-left.svg", "assets/format/tag.svg", - "generated/common.svg", - "generated/format.svg", + "generated/assets/common.svg", + "generated/assets/format.svg", "generated/sprite-info.ts", ] `; -exports[`examples > "groups-with-root" example should replay same output 1`] = ` +exports[`examples > "groups-without-root" example should replay same output 1`] = ` [ [ - "generated/common.svg", + "generated/assets/common.svg", "2eb4b56ff266279ca9c0af3b16f7f3114ae35c4555657bb106acbd15ff9d4f52", ], [ - "generated/format.svg", + "generated/assets/format.svg", "c890959d268bf53d6006012438788b4104189d3cfaf997b0b43b2fd4f1a41159", ], [ "generated/sprite-info.ts", - "fac807c42c630ce77d6e1fea77b808338cf68776235c48736f3b82aa09e761df", + "6280072f9bbc11d77c987a1ce220c9a70197c7612119b86606025455d3feae97", ], ] `; -exports[`examples > "groups-without-root" example should generate files 1`] = ` +exports[`examples > "metadata" example should generate files 1`] = ` [ "assets/common/close.svg", "assets/common/favourite.svg", "assets/format/align-left.svg", "assets/format/tag.svg", - "generated/assets/common.svg", - "generated/assets/format.svg", - "generated/sprite-info.ts", + "generated/common.2eb4b56f.svg", + "generated/format.c890959d.svg", + "generated/meta.ts", ] `; -exports[`examples > "groups-without-root" example should replay same output 1`] = ` +exports[`examples > "metadata" example should replay same output 1`] = ` [ [ - "generated/assets/common.svg", + "generated/common.2eb4b56f.svg", "2eb4b56ff266279ca9c0af3b16f7f3114ae35c4555657bb106acbd15ff9d4f52", ], [ - "generated/assets/format.svg", + "generated/format.c890959d.svg", "c890959d268bf53d6006012438788b4104189d3cfaf997b0b43b2fd4f1a41159", ], [ - "generated/sprite-info.ts", - "1153d305b1db1f1605d0d5e9b30a19ec4fea65b221553ef862158f31c2270de8", + "generated/meta.ts", + "789b7ca4856d0b2d1b105e8267a5950f77d4928ef1710b4db05438cefd96c27f", ], ] `; @@ -178,7 +178,7 @@ exports[`examples > "react" example should replay same output 1`] = ` ], [ "generated/sprite-info.ts", - "fac807c42c630ce77d6e1fea77b808338cf68776235c48736f3b82aa09e761df", + "0c82bd5eb1a18ea3337261408254d09db5cee2c8eeb4561ed6bca996b0e0d9c9", ], ] `; @@ -200,7 +200,7 @@ exports[`examples > "simple" example should replay same output 1`] = ` ], [ "generated/sprite-info.ts", - "89e0e5e36909c3dce4af5977af027d8779b20860232d34612d27a7778bdcc1e3", + "528cef17918096f4bec20bf6e16ff1d970c3999eb5b405469661a12b24c7cac4", ], ] `; diff --git a/libs/svg/src/__tests__/__snapshots__/plugins.test.ts.snap b/libs/svg/src/__tests__/__snapshots__/plugins.test.ts.snap new file mode 100644 index 0000000..2fcf5fd --- /dev/null +++ b/libs/svg/src/__tests__/__snapshots__/plugins.test.ts.snap @@ -0,0 +1,219 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`plugins system > "metadata" plugin > should generate basic runtime info with metadata.runtime as constant name 1`] = ` +"export interface SpritesMap { + 'a': 'a' | 'b', +'b': 'a' | 'b' + } +export const RuntimeExample = { + 'a': ['a', +'b'], +'b': ['a', +'b'] + } satisfies { + 'a': Array<'a' | 'b'>, +'b': Array<'a' | 'b'> + };" +`; + +exports[`plugins system > "metadata" plugin > should generate runtime with size 1`] = ` +"export interface SpritesMap { + 'a': 'a' | 'b', +'b': 'a' | 'b' + } +export const SPRITES_META = { + 'a': { + filePath: 'a.svg', + items: { + 'a': { + + width: 16, height: 16, + }, +'b': { + + width: 16, height: 16, + } + } +}, +'b': { + filePath: 'b.svg', + items: { + 'a': { + + width: 16, height: 16, + }, +'b': { + + width: 16, height: 16, + } + } +} + } satisfies Record + }>;" +`; + +exports[`plugins system > "metadata" plugin > should generate runtime with size, viewBox and custom name 1`] = ` +"export interface SpritesMap { + 'a': 'a' | 'b', +'b': 'a' | 'b' + } +export const RuntimeExample = { + 'a': { + filePath: 'a.svg', + items: { + 'a': { + viewBox: '0 0 16 16', + width: 16, height: 16, + }, +'b': { + viewBox: '0 0 16 16', + width: 16, height: 16, + } + } +}, +'b': { + filePath: 'b.svg', + items: { + 'a': { + viewBox: '0 0 16 16', + width: 16, height: 16, + }, +'b': { + viewBox: '0 0 16 16', + width: 16, height: 16, + } + } +} + } satisfies Record + }>;" +`; + +exports[`plugins system > "metadata" plugin > should generate runtime with viewBox 1`] = ` +"export interface SpritesMap { + 'a': 'a' | 'b', +'b': 'a' | 'b' + } +export const SPRITES_META = { + 'a': { + filePath: 'a.svg', + items: { + 'a': { + viewBox: '0 0 16 16', + + }, +'b': { + viewBox: '0 0 16 16', + + } + } +}, +'b': { + filePath: 'b.svg', + items: { + 'a': { + viewBox: '0 0 16 16', + + }, +'b': { + viewBox: '0 0 16 16', + + } + } +} + } satisfies Record + }>;" +`; + +exports[`plugins system > "metadata" plugin > should generate runtime without types 1`] = ` +"export const RuntimeExample = { + 'a': { + filePath: 'a.svg', + items: { + 'a': { + viewBox: '0 0 16 16', + width: 16, height: 16, + }, +'b': { + viewBox: '0 0 16 16', + width: 16, height: 16, + } + } +}, +'b': { + filePath: 'b.svg', + items: { + 'a': { + viewBox: '0 0 16 16', + width: 16, height: 16, + }, +'b': { + viewBox: '0 0 16 16', + width: 16, height: 16, + } + } +} + };" +`; + +exports[`plugins system > "metadata" plugin > should generate same default output with metadata as string and as { path } 1`] = ` +"export interface SpritesMap { + 'a': 'a' | 'b', +'b': 'a' | 'b' + } +export const SPRITES_META = { + 'a': ['a', +'b'], +'b': ['a', +'b'] + } satisfies { + 'a': Array<'a' | 'b'>, +'b': Array<'a' | 'b'> + };" +`; + +exports[`plugins system > "metadata" plugin > should generate types and runtime at the same time 1`] = ` +"export interface TypesExample { + 'a': 'a' | 'b', +'b': 'a' | 'b' + } +export const RuntimeExample = { + 'a': ['a', +'b'], +'b': ['a', +'b'] + } satisfies { + 'a': Array<'a' | 'b'>, +'b': Array<'a' | 'b'> + };" +`; + +exports[`plugins system > "metadata" plugin > should generate types with metadata.types as interface name 1`] = ` +"export interface TypesExample { + 'a': 'a' | 'b', +'b': 'a' | 'b' + } +export const SPRITES_META = { + 'a': ['a', +'b'], +'b': ['a', +'b'] + } satisfies { + 'a': Array<'a' | 'b'>, +'b': Array<'a' | 'b'> + };" +`; diff --git a/libs/svg/src/__tests__/examples.test.ts b/libs/svg/src/__tests__/examples.test.ts index 520b0e2..625a269 100644 --- a/libs/svg/src/__tests__/examples.test.ts +++ b/libs/svg/src/__tests__/examples.test.ts @@ -52,11 +52,18 @@ describe('examples', () => { } ] }, - 'experimental-runtime': { + metadata: { root: 'assets', group: true, fileName: '{name}.{hash:8}.svg', - experimentalRuntime: true + metadata: { + path: 'generated/meta.ts', + types: true, + runtime: { + viewBox: true, + size: true + } + } } }; diff --git a/libs/svg/src/__tests__/plugins.test.ts b/libs/svg/src/__tests__/plugins.test.ts index a2acd8a..d1df637 100644 --- a/libs/svg/src/__tests__/plugins.test.ts +++ b/libs/svg/src/__tests__/plugins.test.ts @@ -2,18 +2,46 @@ import { toArray } from '@neodx/std'; import { createTmpVfs } from '@neodx/vfs/testing-utils'; import { describe, expect, test } from 'vitest'; import type { SvgFile, SvgNode } from '..'; +import { buildSprites } from '..'; import { groupSprites } from '../plugins'; +import type { MetadataPluginParams } from '../plugins/metadata'; import { combinePlugins } from '../plugins/plugin-utils'; describe('plugins system', async () => { + const defaultSpritesConfig = { + a: ['a/a', 'a/b'], + b: ['b/a', 'b/b'] + }; const emptyContext = { vfs: await createTmpVfs() }; const file = (path: string): SvgFile => ({ name: path, path: `${path}.svg`, + meta: {}, node: {} as SvgNode, - content: '' + content: + '\n' + + ' \n' + + '' }); const files = (...paths: Array) => paths.flatMap(toArray).map(file); + const sprites = (map: Record) => + new Map( + Object.entries(map).map(([name, contents]) => [name, { name, files: files(...contents) }]) + ); + const createTestContext = async ( + spritesConfig: Record = defaultSpritesConfig + ) => { + const map = sprites(spritesConfig); + const vfs = await createTmpVfs({ + initialFiles: { + ...Array.from(map.values()) + .flatMap(({ files }) => files) + .reduce((acc, { path, content }) => ({ ...acc, [path]: content }), {}) + } + }); + + return { vfs, map }; + }; test('resolveEntriesMap should return new map', () => { const hooks = combinePlugins([ @@ -28,7 +56,7 @@ describe('plugins system', async () => { ]); }); - test('groupSprites should reorganize sprites', () => { + test('"groupSprites" plugin should reorganize sprites', () => { const original = new Map([['a', { name: 'a', files: files('a/a', 'a/b', 'b/a') }]]); const hooks = combinePlugins([{ name: 'a' }, groupSprites(), { name: 'b' }]); @@ -37,4 +65,133 @@ describe('plugins system', async () => { ['b', { name: 'b', files: files('b/a') }] ]); }); + + describe('"metadata" plugin', () => { + async function buildWithMetadata(metadata: MetadataPluginParams) { + const { vfs } = await createTestContext(); + + await buildSprites({ + vfs, + input: ['**/*.svg'], + group: true, + output: 'public', + keepTreeChanges: true, + metadata + }); + await vfs.formatChangedFiles(); + + return { vfs }; + } + + test('should disable with metadata: false', async () => { + const { vfs } = await buildWithMetadata(false); + + expect(await vfs.exists('metadata.json')).toBe(false); + }); + + test('should generate same default output with metadata as string and as { path }', async () => { + const asString = await buildWithMetadata('runtime.ts'); + const asPath = await buildWithMetadata({ + path: 'runtime.ts' + }); + + expect(await asString.vfs.read('runtime.ts', 'utf-8')).toEqual( + await asPath.vfs.read('runtime.ts', 'utf-8') + ); + expect(await asString.vfs.read('runtime.ts', 'utf-8')).toMatchSnapshot(); + }); + + test('should skip output when everything is disabled', async () => { + const { vfs } = await buildWithMetadata({ + path: 'runtime.ts', + types: false, + runtime: false + }); + + expect(await vfs.exists('runtime.ts')).toBe(false); + }); + + test('should generate types with metadata.types as interface name', async () => { + const { vfs } = await buildWithMetadata({ + path: 'runtime.ts', + types: 'TypesExample' + }); + const content = await vfs.read('runtime.ts', 'utf-8'); + + expect(content).toContain('export interface TypesExample {'); + expect(content).toMatchSnapshot(); + }); + + test('should generate basic runtime info with metadata.runtime as constant name', async () => { + const { vfs } = await buildWithMetadata({ + path: 'runtime.ts', + runtime: 'RuntimeExample' + }); + const content = await vfs.read('runtime.ts', 'utf-8'); + + expect(content).toContain('export const RuntimeExample = {'); + expect(content).toMatchSnapshot(); + }); + + test('should generate types and runtime at the same time', async () => { + const { vfs } = await buildWithMetadata({ + path: 'runtime.ts', + types: 'TypesExample', + runtime: 'RuntimeExample' + }); + const content = await vfs.read('runtime.ts', 'utf-8'); + + expect(content).toContain('export interface TypesExample {'); + expect(content).toContain('export const RuntimeExample = {'); + expect(content).toMatchSnapshot(); + }); + + test('should generate runtime with size', async () => { + const { vfs } = await buildWithMetadata({ + path: 'runtime.ts', + runtime: { + size: true + } + }); + + expect(await vfs.read('runtime.ts', 'utf-8')).toMatchSnapshot(); + }); + + test('should generate runtime with viewBox', async () => { + const { vfs } = await buildWithMetadata({ + path: 'runtime.ts', + runtime: { + viewBox: true + } + }); + + expect(await vfs.read('runtime.ts', 'utf-8')).toMatchSnapshot(); + }); + + test('should generate runtime with size, viewBox and custom name', async () => { + const { vfs } = await buildWithMetadata({ + path: 'runtime.ts', + runtime: { + name: 'RuntimeExample', + size: true, + viewBox: true + } + }); + + expect(await vfs.read('runtime.ts', 'utf-8')).toMatchSnapshot(); + }); + + test('should generate runtime without types', async () => { + const { vfs } = await buildWithMetadata({ + path: 'runtime.mjs', + runtime: { + name: 'RuntimeExample', + size: true, + viewBox: true + } + }); + + expect(await vfs.read('runtime.mjs', 'utf-8')).toMatchSnapshot(); + }); + }); }); diff --git a/libs/svg/src/__tests__/testing-utils.ts b/libs/svg/src/__tests__/testing-utils.ts index 1d9c679..60f308e 100644 --- a/libs/svg/src/__tests__/testing-utils.ts +++ b/libs/svg/src/__tests__/testing-utils.ts @@ -32,7 +32,7 @@ export async function generateExample( input: ['**/*.svg'], output: 'generated', optimize: true, - definitions: 'generated/sprite-info.ts', + metadata: 'generated/sprite-info.ts', keepTreeChanges: !write, ...options }); diff --git a/libs/svg/src/core/create-sprite-builder.ts b/libs/svg/src/core/create-sprite-builder.ts index c627e0d..1243780 100644 --- a/libs/svg/src/core/create-sprite-builder.ts +++ b/libs/svg/src/core/create-sprite-builder.ts @@ -4,11 +4,13 @@ import { compact, isTruthy, quickPluralize } from '@neodx/std'; import type { VFS } from '@neodx/vfs'; import { basename, join } from 'pathe'; import { parse } from 'svgson'; -import type { ResetColorsPluginParams } from '../plugins'; -import { fixViewBox, groupSprites, resetColors, setId, svgo, typescript } from '../plugins'; +import { fixViewBox, groupSprites, legacyTypescript, resetColors, setId, svgo } from '../plugins'; +import { extractSvgMeta } from '../plugins/fix-view-box'; +import { metadata as metadataPlugin, type MetadataPluginParams } from '../plugins/metadata'; import { combinePlugins } from '../plugins/plugin-utils'; +import type { ResetColorsPluginParams } from '../plugins/reset-colors'; import { renderSvgNodesToString } from './render'; -import type { GeneratedSprites, SvgFile, SvgNode } from './types'; +import type { GeneratedSprites, SvgFile, SvgFileMeta, SvgNode } from './types'; export interface CreateSpriteBuilderParams { vfs: VFS; @@ -44,8 +46,17 @@ export interface CreateSpriteBuilderParams { * Should we optimize icons? */ optimize?: boolean; + /** + * Configures metadata generation + * @example "src/sprites/meta.ts" + * @example { path: "meta.ts", runtime: false } // will generate only types + * @example { path: "meta.ts", types: 'TypeName', runtime: 'InfoName' } // will generate "interface TypeName" types and "const InfoName" runtime metadata + * @example { path: "meta.ts", runtime: { size: true, viewBox: true } } // will generate runtime metadata with size and viewBox + */ + metadata?: MetadataPluginParams; /** * Path to generated definitions file + * @deprecated use `metadata` instead */ definitions?: string; /** @@ -56,6 +67,7 @@ export interface CreateSpriteBuilderParams { * WILL BE CHANGED IN FUTURE * Replaces current approach (just array of IDs per sprite) with extended runtime metadata * + * @deprecated use `metadata` instead * @unstable * @example * export const SPRITES_META = { @@ -85,12 +97,19 @@ export function createSpriteBuilder({ output, logger, group: enableGroup, + metadata, fileName: fileNameTemplate = '{name}.svg', optimize, definitions, resetColors: resetColorsParams, experimentalRuntime }: CreateSpriteBuilderParams) { + if (definitions || experimentalRuntime) { + logger?.error( + 'DEPRECATED: `definitions` and `experimentalRuntime` options will be removed in future versions, use `metadata` instead' + ); + } + const hooks = combinePlugins( compact([ enableGroup && groupSprites(), @@ -98,8 +117,10 @@ export function createSpriteBuilder({ fixViewBox(), resetColors(resetColorsParams), optimize && svgo(), - definitions && - typescript({ + !definitions && metadataPlugin(metadata), + !metadata && + definitions && + legacyTypescript({ output: definitions, experimentalRuntime }) @@ -119,11 +140,15 @@ export function createSpriteBuilder({ return null; } try { - const nodeToFile = (node: SvgNode): SvgFile => ({ name, node, path, content }); + let meta: SvgFileMeta; + const nodeToFile = (node: SvgNode): SvgFile => ({ name, meta, node, path, content }); const node = await parse(await hooks.transformSourceContent(path, content), { camelcase: true, - transformNode: node => hooks.transformNode(nodeToFile(node)) + transformNode: node => { + meta = extractSvgMeta(node); + return hooks.transformNode(nodeToFile(node)); + } }); return nodeToFile(node); diff --git a/libs/svg/src/core/types.ts b/libs/svg/src/core/types.ts index ba61846..cfe46e7 100644 --- a/libs/svg/src/core/types.ts +++ b/libs/svg/src/core/types.ts @@ -39,12 +39,19 @@ export interface GeneratedSprite extends SpriteGroup { } export interface SvgFile { + meta: SvgFileMeta; node: SvgNode; path: string; name: string; content: string; } +export interface SvgFileMeta { + width?: number; + height?: number; + viewBox?: string; +} + export interface SvgNode { name: string; type: string; diff --git a/libs/svg/src/plugins/fix-view-box.ts b/libs/svg/src/plugins/fix-view-box.ts index 0d09524..3f90fec 100644 --- a/libs/svg/src/plugins/fix-view-box.ts +++ b/libs/svg/src/plugins/fix-view-box.ts @@ -1,4 +1,5 @@ import { omit } from '@neodx/std'; +import type { SvgFileMeta, SvgNode } from '../core'; import { createPlugin } from './plugin-utils'; /** @@ -6,17 +7,33 @@ import { createPlugin } from './plugin-utils'; */ export const fixViewBox = () => createPlugin('fix-view-box', { - transformNode({ node }) { - const { - attributes: { viewBox, width, height } - } = node; - + transformNode({ node, meta: { viewBox } }) { return { ...node, attributes: { ...omit(node.attributes, ['width', 'height']), - viewBox: viewBox || !width || !height ? viewBox : `0 0 ${width} ${height}` + viewBox: viewBox as string } }; } }); + +export function extractSvgMeta({ + attributes: { width: originalWidth, height: originalHeight, viewBox } +}: SvgNode): SvgFileMeta { + const [parsedWidth, parsedHeight] = viewBox ? parseViewBox(viewBox) : []; + const width = originalWidth ? Number.parseFloat(originalWidth) : parsedWidth; + const height = originalHeight ? Number.parseFloat(originalHeight) : parsedHeight; + + return { + width, + height, + viewBox: viewBox || (width && height ? `0 0 ${width} ${height}` : undefined) + }; +} + +const parseViewBox = (viewBox: string): [number, number] => { + const [, , width, height] = viewBox.split(' ').map(Number.parseFloat); + + return [width, height]; +}; diff --git a/libs/svg/src/plugins/index.ts b/libs/svg/src/plugins/index.ts index c6c2b96..228ddca 100644 --- a/libs/svg/src/plugins/index.ts +++ b/libs/svg/src/plugins/index.ts @@ -1,5 +1,6 @@ export { fixViewBox } from './fix-view-box'; export { type GroupPluginOptions, groupSprites } from './group'; +export { legacyTypescript, type LegacyTypescriptPluginOptions } from './legacy-typescript'; export type { AnyColorInput, ColorPropertyReplacementInput, @@ -9,4 +10,3 @@ export type { export { resetColors } from './reset-colors'; export { setId } from './set-id'; export { svgo, type SvgoPluginOptions } from './svgo'; -export { typescript, type TypescriptPluginOptions } from './typescript'; diff --git a/libs/svg/src/plugins/typescript.ts b/libs/svg/src/plugins/legacy-typescript.ts similarity index 91% rename from libs/svg/src/plugins/typescript.ts rename to libs/svg/src/plugins/legacy-typescript.ts index 2832141..6da1241 100644 --- a/libs/svg/src/plugins/typescript.ts +++ b/libs/svg/src/plugins/legacy-typescript.ts @@ -1,19 +1,19 @@ import type { GeneratedSprite, GeneratedSprites, SvgFile } from '../core'; import { createPlugin } from './plugin-utils'; -export interface TypescriptPluginOptions { +export interface LegacyTypescriptPluginOptions { output?: string; metaName?: string; typeName?: string; experimentalRuntime?: boolean; } -export function typescript({ +export function legacyTypescript({ output = 'sprite.types.ts', typeName = 'SpritesMap', metaName = 'SPRITES_META', experimentalRuntime -}: TypescriptPluginOptions = {}) { +}: LegacyTypescriptPluginOptions = {}) { return createPlugin('typescript', { async afterWriteAll(entries, context) { await context.vfs.write( @@ -80,11 +80,7 @@ const renderSpriteAsExperimentalRuntimeMeta = (sprite: GeneratedSprite) => } }`; -const renderSvgFileAsExperimentalRuntimeMeta = ({ - node: { - attributes: { viewBox } - } -}: SvgFile) => +const renderSvgFileAsExperimentalRuntimeMeta = ({ meta: { viewBox } }: SvgFile) => `{ viewBox: '${viewBox}', }`; diff --git a/libs/svg/src/plugins/metadata.ts b/libs/svg/src/plugins/metadata.ts new file mode 100644 index 0000000..157557a --- /dev/null +++ b/libs/svg/src/plugins/metadata.ts @@ -0,0 +1,143 @@ +import type { GeneratedSprite, GeneratedSprites, SvgFile } from '../core'; +import { createPlugin } from './plugin-utils'; + +export type MetadataPluginParams = false | string | MetadataPluginParamsConfig; + +export interface MetadataPluginParamsConfig { + path: string; + types?: Partial | boolean | string; + runtime?: Partial | boolean | string; +} + +export interface MetadataTypesParams { + name: string; +} + +export interface MetadataRuntimeParams { + name: string; + size?: boolean; + viewBox?: boolean; +} + +export function metadata(params: MetadataPluginParams = false) { + if (!params) return null; + const config = Object.assign( + { + runtime: true, + types: true + }, + typeof params === 'string' ? { path: params } : params + ); + const types = toNamedParams({ name: 'SpritesMap' }, config.types); + const runtime = toNamedParams({ name: 'SPRITES_META' }, config.runtime); + const isTypeScript = config.path.endsWith('.ts'); + + if (!types && !runtime) return null; + return createPlugin('metadata', { + async afterWriteAll(sprites, context) { + await context.vfs.write( + config.path, + [ + isTypeScript && types && renderTypes(types, sprites), + runtime && renderRuntime(runtime, sprites, isTypeScript) + ] + .filter(Boolean) + .join('\n') + ); + } + }); +} + +const renderTypes = ({ name }: MetadataTypesParams, sprites: GeneratedSprites) => + `export interface ${name} { + ${renderIterableAsRecord( + sprites.values(), + sprite => sprite.name, + ({ files }) => files.map(file => stringLiteral(file.name)).join(' | ') + )} + }`; + +const renderRuntime = ( + params: MetadataRuntimeParams, + sprites: GeneratedSprites, + isTypeScript: boolean +) => { + const { name, size, viewBox } = params; + const detailedRuntime = Boolean(size || viewBox); + const satisfies = detailedRuntime + ? `Record + }>` + : `{ + ${renderIterableAsRecord( + sprites.values(), + sprite => sprite.name, + ({ files }) => `Array<${files.map(file => stringLiteral(file.name)).join(' | ')}>` + )} + }`; + + return `export const ${name} = { + ${renderIterableAsRecord( + sprites.values(), + sprite => sprite.name, + sprite => + detailedRuntime ? renderRuntimeAsDetails(sprite, params) : renderRuntimeAsArray(sprite) + )} + }${isTypeScript ? ` satisfies ${satisfies}` : ''};`; +}; + +const renderRuntimeAsArray = ({ files }: GeneratedSprite) => + `[${files.map(file => stringLiteral(file.name)).join(',\n')}]`; + +const renderRuntimeAsDetails = ( + { files, filePath }: GeneratedSprite, + params: MetadataRuntimeParams +) => `{ + filePath: '${filePath}', + items: { + ${renderIterableAsRecord( + files, + file => file.name, + file => renderMetadata(params, file) + )} + } +}`; + +const renderMetadata = ( + { size, viewBox: displayViewBox }: MetadataRuntimeParams, + { meta: { width, height, viewBox } }: SvgFile +) => + `{ + ${displayViewBox ? `viewBox: '${viewBox}',` : ''} + ${size ? `width: ${width}, height: ${height},` : ''} + }`; + +const toNamedParams = ( + defaultValue: T, + params: Partial | boolean | string +): T | null => { + if (!params) return null; + if (typeof params === 'string' || typeof params === 'boolean') { + return { ...defaultValue, name: params === true ? defaultValue.name : params } as T; + } + return { + ...defaultValue, + ...params + }; +}; + +const renderIterableAsRecord = ( + items: Iterable, + key: (item: T) => string, + value: (item: T) => string, + separator = ',\n' +) => + Array.from(items) + .map(item => `${stringLiteral(key(item))}: ${value(item)}`) + .join(separator); + +const stringLiteral = (value: string) => `'${value}'`;