Skip to content

Commit

Permalink
Enhance bezier-figma-plugin by using github action (#1596)
Browse files Browse the repository at this point in the history
<!--
  How to write a good PR title:
- Follow [the Conventional Commits
specification](https://www.conventionalcommits.org/en/v1.0.0/).
  - Give as much context as necessary and as little as possible
  - Prefix it with [WIP] while it’s a work in progress
-->

## Self Checklist

- [x] I wrote a PR title in **English**.
- [x] I added an appropriate **label** to the PR.
- [x] I wrote a commit message in **English**.
- [x] I wrote a commit message according to [**the Conventional Commits
specification**](https://www.conventionalcommits.org/en/v1.0.0/).
- [x] [I added the appropriate
**changeset**](https://github.com/channel-io/bezier-react/blob/main/CONTRIBUTING.md#add-a-changeset)
for the changes.
- [ ] ~~[Component] I wrote **a unit test** about the implementation.~~
- [ ] ~~[Component] I wrote **a storybook document** about the
implementation.~~
- [ ] ~~[Component] I tested the implementation in **various
browsers**.~~
  - Windows: Chrome, Edge, (Optional) Firefox
  - macOS: Chrome, Edge, Safari, (Optional) Firefox
- [ ] ~~[*New* Component] I added my username to the correct directory
in the `CODEOWNERS` file.~~

## Related Issue

- #863 
- changeset 자동생성과 description 생성 기능까지 추가하면 resolve 해도 될 것 같습니다.

## Summary
<!-- Please add a summary of the modification. -->

- `bezier-figma-pluain`의 실행 속도와 구조를 개선합니다.
- 아이콘 마다 git blob 을 만드는 과정을 없애고 깃헙 액션에서 아이콘을 생성하도록 합니다.

## Demo

**개인 레포지토리 타겟으로 동작 시연 영상**


https://github.com/channel-io/bezier-react/assets/28595102/67927889-6a82-463a-aa00-d266c6b38240

**[깃헙 액션 다 돌고난
후](https://github.com/yangwooseong/bezier-react/pull/39/files)**

## Details
<!-- Please add a detailed description of the modification. (such as
AS-IS/TO-DO)-->


- 기존의 플러그인 동작방식은, 플러그인 피그마 API 를 통해 선택한 아이콘들을 모두 svg 로 변환하여 이걸 git blob
으로 변환한 후 github REST API 를 통해 pr 을 만드는 과정을 거쳤습니다. 그런데 500개가 넘는 아이콘에 대하여
`/git/blobs` API를 모두 호출해야 해서 시간이 많이 소요되었습니다. (대략 9분정도 소요)
- 이번 pr 로 동작방식이 다음과 같이 변경되었습니다. 
  1. `Node.exportAsync` 메서드를 통해 선택한 아이콘들을 svg string으로 변환
  2. 아이콘 이름과 svg string을 키 밸류로 하는 json 객체(`icons.json`)생성
  3. github API 를 통해 `icons.json`만 변경사항으로 가지는 pr 생성
4. pr 이 생성되면 github action 이 트리거 되어 `icons.json` 파일로부터 각 아이콘 이름을 파일
이름으로, svg string 을 파일 내용으로 하여 아이콘 파일을 생성하는
스크립트(`generate-icon-files.js`) 실행
  5. `icons.json` 삭제하는 커밋 생성
- 이 과정이 다 도는데 10초 정도가 걸리게 됩니다. 또한, `icons.json`을 보고 추가로 github action 을
트리거할 수 있기 때문에, changeset 생성이나 적절한 description 생성같은 기능도 추후에 덧붙이기 용이한 구조로
변경되었습니다.

- 기존의 플러그인이 생성한 icon 파일과 변경사항이 없기를 원했지만, `devices.svg` 파일 한개에 대해서는 diff
가 발생하는 것을 막지
못했습니다([#](https://github.com/yangwooseong/bezier-react/pull/39/files#diff-e7d3a3a633d92885f712dc52585180368d0d7bcc0d02da7e4b0de47e108040fd)).
아마 figmaAPI를 호출해서 svg 로 변환하는 것과 이번에 사용한 `Node.exportAsync` 의 결과물이 완전히
일치하지 않아서 그런 것 같습니다. 겉으로 보이는 것은 완전히 동일해서 diff 가 생겨도 무방하지 않을까 생각합니다만, 새로운
플러그인이 배포되면 한번 조정하는 작업이 필요해 보입니다.

## Breaking change or not (Yes/No)
<!-- If Yes, please describe the impact and migration path for users -->
No

## References
<!-- External documents based on workarounds or reviewers should refer
to -->
- 주로
https://junghyeonsu.com/posts/quickly-apply-icons-that-exist-in-figma-to-your-dev-team/
이 블로그를 참고하였습니다.
- https://www.figma.com/plugin-docs/how-plugins-run/
- https://docs.github.com/en/rest/git?apiVersion=2022-11-28
  • Loading branch information
yangwooseong authored Sep 11, 2023
1 parent 772d763 commit 9d44070
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 212 deletions.
7 changes: 7 additions & 0 deletions .changeset/dull-pillows-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"bezier-figma-plugin": minor
---

Enhance bezier-figma-plugin running performance
- Remove svg extract process using FigmaAPI and merely send json file that contains svg string
- Make icon files based on given json file during Github Action
5 changes: 5 additions & 0 deletions .changeset/healthy-kiwis-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@channel.io/bezier-icons": minor
---

Add script for generating svg file from icons.json
38 changes: 38 additions & 0 deletions .github/workflows/generate-icon-files.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Generate icon files from icons.json file

on:
push:
branches:
- icon-update-*
paths:
- packages/bezier-icons/icons.json

jobs:
generate-svg:
name: Generate icon files from icons.json file
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18.17.1

- name: Git Config
run: |
git config --global user.email "[email protected]"
git config --global user.name "ch-builder"
- name: Generate Svg files from icons.json
run: |
node packages/bezier-icons/scripts/generate-icon-files.js
git add .
git commit -m "feat(bezier-icons): generate icon files from icons.json"
- name: Delete icons.json files
run: |
git rm packages/bezier-icons/icons.json
git commit -m "feat(bezier-icons): remove icons.json"
git push
33 changes: 20 additions & 13 deletions packages/bezier-figma-plugin/src/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
import {
type ExtractIconPluginMessage,
type GetTokenPluginMessage,
type SvgByName,
type UIMessage,
} from '../types/Message'

import {
findAllComponentNode,
flatten,
} from './utils'
import { findAllComponentNode } from './utils'

// eslint-disable-next-line no-console
console.info('Figma file key: ', figma.fileKey)

figma.showUI(__html__, { width: 400, height: 300 })

function extractIcon() {
async function extractIcon() {
const componentNodes = figma.currentPage.selection
.map(findAllComponentNode)
.reduce(flatten, [])
.map(({ id, name }) => ({ id, name }))
.flatMap(v => v)

const svgs = await Promise.all(componentNodes.map((
async (node) => {
const svg = await node.exportAsync({ format: 'SVG_STRING' })
const id = node.name
return {
svg,
id,
}
}
)))

const componentNodesIdsQuery = componentNodes
.map(({ id }) => id)
.join(',')
const svgByName = svgs.reduce((acc, cur) => {
acc[cur.id] = cur
return acc
}, {} as SvgByName)

const pluginMessage: ExtractIconPluginMessage = {
type: 'extractIcon',
payload: {
fileKey: figma.fileKey as string,
ids: componentNodesIdsQuery,
nodes: componentNodes,
svgByName,
},
}

Expand Down
2 changes: 0 additions & 2 deletions packages/bezier-figma-plugin/src/plugin/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export const flatten = <T>(a: T[], b: T[]) => [...a, ...b]

export const isComponentNode = (node: SceneNode): node is ComponentNode => node.type === 'COMPONENT'

export const findAllComponentNode = (rootNode: SceneNode) => {
Expand Down
5 changes: 2 additions & 3 deletions packages/bezier-figma-plugin/src/types/Message.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
export type SvgByName = { [id: string]: object }
export interface ExtractIconPluginMessage {
type: 'extractIcon'
payload: {
fileKey: string
ids: string
nodes: Array<{ id: string, name: string }>
svgByName: SvgByName
}
}

Expand Down
159 changes: 7 additions & 152 deletions packages/bezier-figma-plugin/src/ui/components/IconExtract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ import {

import config from '../../config'
import type { PluginMessage } from '../../types/Message'
import useFigmaAPI from '../hooks/useFigmaAPI'
import useGithubAPI from '../hooks/useGithubAPI'
import { createSvgGitBlob } from '../utils'
import { useCreatePRWithSvgMap } from '../hooks/useCreatePRWithSvgMap'

enum Step {
Pending,
Expand All @@ -42,7 +40,7 @@ interface ProgressProps {
onError: (msg: string) => void
}

function useProgress() {
export function useProgress() {
const [progressTitle, setProgressTitle] = useState('')
const [progressValue, setProgressValue] = useState(0)

Expand Down Expand Up @@ -85,160 +83,17 @@ function Progress({
progressValue,
} = useProgress()

const figmaAPI = useFigmaAPI({ token: figmaToken })

const githubAPI = useGithubAPI({
auth: githubToken,
owner: config.repository.owner,
repo: config.repository.name,
})
const createPr = useCreatePRWithSvgMap({ progress, githubToken })

useEffect(function bindOnMessageHandler() {
window.onmessage = async (event: MessageEvent<PluginMessage>) => {
const { type, payload } = event.data.pluginMessage

if (type === 'extractIcon') {
try {
const { fileKey, ids, nodes } = payload

const getSvgImagesFromFigma = async () => {
const { images } = await figmaAPI.getSvg({ fileKey, ids })
if (!images) {
throw new Error('선택된 아이콘이 없거나 잘못된 피그마 토큰입니다.')
}
return images
}

const createSvgGitBlobsFromSvgImages = (images: Record<string, string>) => async () => {
const gitBlobs = await Promise.all(
nodes.map(async ({ id, name }) => {
const response = await fetch(images[id])
const svg = await response.text()
const { sha } = await githubAPI.createGitBlob(svg)
return { name, sha }
}),
)

const svgGitBlobs = gitBlobs.reduce((acc, { name, sha }) => {
const path = `${name}.svg`
return { ...acc, [path]: createSvgGitBlob(path, sha) }
}, {} as Record<string, ReturnType<typeof createSvgGitBlob>>)

return svgGitBlobs
}

const createNewGitCommitFromSvgGitBlobs = (svgGitBlobs: Record<string, ReturnType<typeof createSvgGitBlob>>) => async () => {
const newSvgBlobs = Object.values(svgGitBlobs)

const baseRef = await githubAPI.getGitRef(config.repository.baseBranchName)
const headCommit = await githubAPI.getGitCommit(baseRef.sha)
const headTree = await githubAPI.getGitTree(headCommit.sha)

const splittedPaths = config.repository.iconExtractPath.split('/')

const parentTrees: Awaited<ReturnType<typeof githubAPI['getGitTree']>>[] = []

const prevSvgBlobsTree = await splittedPaths.reduce(async (parentTreePromise, splittedPath) => {
const parentTree = await parentTreePromise
const targetTree = parentTree.find(({ path }) => path === splittedPath)
if (!targetTree || !targetTree.sha) {
throw new Error(`${splittedPath} 경로가 없습니다. 올바른 경로를 입력했는지 확인해주세요.`)
}
parentTrees.push(parentTree)
return githubAPI.getGitTree(targetTree.sha)
}, Promise.resolve(headTree))

const newSvgBlobsTree = [
...prevSvgBlobsTree.map((blob) => {
const overridedBlob = svgGitBlobs[blob.path as string]
if (overridedBlob) {
delete svgGitBlobs[blob.path as string]
return { ...blob, ...overridedBlob }
}
return null
}).filter(Boolean),
...newSvgBlobs,
]

const newGitSvgTree = await githubAPI.createGitTree({
// @ts-ignore
tree: newSvgBlobsTree,
})

const gitTree = await splittedPaths.reduceRight(async (prevTreePromise, cur, index) => {
const parentTree = parentTrees[index]
const targetTree = parentTree.find(({ path }) => path === cur)
const { sha } = await prevTreePromise
return githubAPI.createGitTree({
tree: [
// @ts-ignore
...parentTree.filter(({ path }) => path !== cur), { ...targetTree, sha },
],
})
}, Promise.resolve(newGitSvgTree))

const now = new Date()
const commit = await githubAPI.createGitCommit({
message: config.commit.message,
author: {
...config.commit.author,
date: now.toISOString(),
},
parents: [headCommit.sha],
tree: gitTree.sha,
})

return commit
}

const createGitPullRequestFromGitCommit = (commit: Awaited<ReturnType<typeof githubAPI['createGitCommit']>>) => async () => {
const now = new Date()
const newBranchName = `update-icons-${now.valueOf()}`

await githubAPI.createGitRef({
branchName: newBranchName,
sha: commit.sha,
})

const { labels, ...rest } = config.pr

const { html_url, number } = await githubAPI.createPullRequest({
...rest,
head: newBranchName,
base: config.repository.baseBranchName,
})

await githubAPI.addLabels({
issueNumber: number,
labels,
})

return html_url
}

const svgImages = await progress({
callback: getSvgImagesFromFigma,
title: '🚚 피그마에서 svg를 가져오는 중...',
successValueOffset: 0.2,
})

const svgGitBlobs = await progress({
callback: createSvgGitBlobsFromSvgImages(svgImages),
title: '📦 svg를 파일로 만드는 중...',
successValueOffset: 0.3,
})

const gitCommit = await progress({
callback: createNewGitCommitFromSvgGitBlobs(svgGitBlobs),
title: '📦 svg 파일을 변환하는 중...',
successValueOffset: 0.3,
})

const pullRequestUrl = await progress({
callback: createGitPullRequestFromGitCommit(gitCommit),
title: '🚚 PR을 업로드하는 중...',
successValueOffset: 0.2,
})
const { svgByName } = payload

const prUrl = await createPr(svgByName)

parent.postMessage({
pluginMessage: {
Expand All @@ -247,7 +102,7 @@ function Progress({
},
}, '*')

navigate('../extract_success', { state: { url: pullRequestUrl } })
navigate('../extract_success', { state: { url: prUrl } })
} catch (e: any) {
onError(e?.message)
}
Expand Down
Loading

0 comments on commit 9d44070

Please sign in to comment.