refactor(sync-figma): convert SVG with SVGR + svgo instead of regex - #715
Conversation
`node` 대신 `tsx`로 실행하도록 바꾸고 `.mjs`/`.js` 11개를 `.ts`로 옮겼다. 빌드 산출물은 만들지 않는다 — 1년에 몇 번 도는 내부 CLI라 dist를 둘 이유가 없다. 타입을 붙이는 과정에서 드러난 문제 세 가지를 함께 고쳤다. - `TYPE`이 비었거나 오타면 `ICON_TYPES[undefined]`를 구조 분해하다 죽었다. FIGMA_TOKEN 검사 옆에 가드를 뒀다. - `sync-icons`의 `else` 분기는 도달할 수 없는 코드였다. `TYPE`은 basic 아니면 symbol뿐이라 앞 분기에서 모두 걸린다. - `lib`의 `hasOwnProperty` 직접 호출을 `Object.hasOwn`으로 바꿨다. `prettier`는 `sync-icons`가 이미 import하고 있었는데 선언만 빠져 있어 dependencies에 넣었다.
Figma SVG를 React 컴포넌트로 바꾸는 일을 정규식 네 단계(`transforms.ts`)가 하고 있었다. 문자열 치환이라 선언이 둘 이상인 `style` 속성에서 깨졌고, `<svg>` 루트와 자식을 구분하지 않고 `fill="none"`을 지워 stroke만 있는 아이콘을 망가뜨렸다. 이 변환을 `@svgr/core`(+`plugin-svgo`, `plugin-jsx`)에 넘긴다. 정규식이 하던 네 가지(크기 제거·속성 camelCase·`style` 객체화·색 비우기)가 전부 라이브러리 기본기로 덮이고, 덤으로 경로 데이터가 최적화된다. 기본값에서 벗어난 설정 세 가지는 각각 주석으로 이유를 남겼다. - `prefixIds`를 명시한다. svgo의 `cleanupIds`가 id를 `a`로 줄여 두 아이콘이 한 페이지에 있으면 마스크가 충돌한다. - `removeViewBox: false` + `removeDimensions` 조합으로 viewBox를 남기고 width/height만 지운다. 크기는 `IconBase`가 정한다. - mono 아이콘은 루트 `fill="none"`을 지운다. 남기면 자식의 `fill="black"`을 제거한 뒤 상속할 색이 없어 아이콘이 사라진다. 커스텀 플러그인 둘을 새로 뒀다. `keepStrokedShapesHollow`는 루트 `fill="none"`이 사라지기 전에 그 의도를 도형에 직접 적고, `strokeFollowsCurrentColor`는 Figma가 박아 넣은 `stroke="black"`을 `currentColor`로 바꿔 stroke도 `color`를 따르게 한다. `prettier` 설정은 하드코딩 대신 저장소 설정을 읽어 쓴다. import 정렬 플러그인까지 걸려서 생성된 파일이 `pnpm format` 결과와 어긋나지 않는다.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
Warning Review limit reachedNext included review available in 36 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| /** | ||
| * Wrap the SVG in IconBase instead of rendering a bare <svg>. | ||
| */ | ||
| const template: Config['template'] = ({ componentName, jsx }, { tpl }) => { |
There was a problem hiding this comment.
IconBase, rest props 추가를 위해 template custom
커스텀하지 않는 경우 svg 태그로 나오기 때문에 IconBase로 나오기 위해 커스텀
| * Figma paints mono icons with a literal black fill (and stroke on a few), which would ignore the | ||
| * colour IconBase hands down. Point both at currentColor; the root fill="none" Figma always emits | ||
| * stays put so stroked shapes keep inheriting `none` and stay hollow. | ||
| */ | ||
| const blackFollowsCurrentColor: SvgoPlugin = { | ||
| name: 'blackFollowsCurrentColor', | ||
| fn: () => ({ | ||
| element: { | ||
| enter: (node) => { | ||
| for (const attr of ['fill', 'stroke'] as const) { | ||
| if (BLACK.test(node.attributes[attr] ?? '')) | ||
| node.attributes[attr] = 'currentColor'; | ||
| } | ||
| }, | ||
| }, | ||
| }), | ||
| }; |
There was a problem hiding this comment.
colorIcon이 아닌 경우 black을 currentColor로 변경해야 theme 대응이 되기 때문에 black, 000, 000000을 모두 currentColor로 대체.
| }): Config => ({ | ||
| typescript: true, | ||
| template, | ||
| plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], |
There was a problem hiding this comment.
순서대로 파이프라인이 진행됩니다.
- svgo를 통해 id 양식 통일과 불필요한 속성 제거
- svgr을 통해 jsx ast 생성
| typescript: true, | ||
| template, | ||
| plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], | ||
| svgoConfig: { |
There was a problem hiding this comment.
svgoConfig를 넘기는 경우 svgr에 설정된 svgo 설정 모두가 대체됩니다.
그래서 기본으로 들어가는 prefixIds를 다시 작성해 주어야 한다.
| plugins: [ | ||
| // removeViewBox would drop the viewBox because it matches width/height; | ||
| // removeDimensions then drops width/height instead, leaving IconBase in charge of size. | ||
| { name: 'preset-default', params: { overrides: { removeViewBox: false } } }, |
There was a problem hiding this comment.
removeViewBox는 사이즈와 viewBox가 동일하면 제거하기 때문에, viewBox는 유지하고 width, height만 제거하도록 설정.
…t/icons-svgr-pipeline
Related Issues
Description of Changes
왜 바꾸나
Figma SVG를 React 컴포넌트로 바꾸는 일을
transforms.ts의 정규식 세 함수가 맡고 있었습니다. 문자열 치환이라 두 군데서 샙니다.remakeMaskStyle은style속성에 선언이 둘 이상이면(mask-type:alpha;opacity:0.5) 앞의 하나만 객체로 바꾸고 나머지는 값 안에 문자열로 남깁니다.makeFlexibleColorIcon은fill="none"을<svg>루트와 자식 구분 없이 지웁니다. Figma는 루트의fill="none"으로 stroke 도형의 속을 비워 두는데, 이 규칙이 그 의도를 지웁니다.무엇을 바꿨나
변환을
@svgr/core(+plugin-svgo,plugin-jsx)에 넘겼습니다. 정규식이 하던 width/height 제거, 속성 camelCase,style객체화가 전부 라이브러리 기본기로 덮입니다. 경로 데이터 최적화는 덤입니다.transforms.ts와 문자열 템플릿icon-component.ts를 지우고svgr.ts하나로 합쳤습니다. 컴포넌트 껍데기는 SVGR custom template이 만들고, 거기서<svg>JSX 요소를IconBase로 바꿔 끼웁니다.커스텀 플러그인
blackFollowsCurrentColor는 mono 아이콘에서fill/stroke의 리터럴 검정(#000,#000000,black)을currentColor로 바꿉니다.IconBase가color를 내려 주므로 이제 fill과 stroke가 함께 그 색을 따라갑니다. color 아이콘은 Figma 팔레트를 그대로 둬야 해서 이 플러그인을 태우지 않습니다.Figma가 항상 붙이는 루트
fill="none"은 건드리지 않습니다. 남겨 두면 stroke만 있는 도형이 그 값을 상속해 속이 빈 채로 남습니다. 지우는 쪽을 먼저 시도했다가 stroke 도형이 꽉 찬 덩어리가 되어 되돌렸습니다(39f4729).검증
pnpm --filter @repo/sync-figma typecheck통과prefixIds적용, 루트fill="none"유지, 검정fill/stroke→currentColorChecklist