Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/real-bears-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bezier-figma-plugin": minor
---

Add feature to automatically add labels.
1 change: 1 addition & 0 deletions packages/bezier-figma-plugin/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const config = {
pr: {
title: 'Extract the icons from Figma',
body: 'Please check the changed part!\n---\nThis Pull Request was generated by bezier-figma-plugin.',
labels: ['feat:icon'],
},
}

Expand Down
255 changes: 163 additions & 92 deletions packages/bezier-figma-plugin/src/ui/components/IconExtract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,48 @@ interface ProgressProps {
onError: (msg: string) => void
}

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

const progress = useCallback(async <Fn extends () => Promise<any>>({
callback,
title,
successValueOffset,
}: {
callback: Fn
title?: string
successValueOffset?: number
}) => {
if (title) {
setProgressTitle(title)
}
const result = await callback()
if (successValueOffset) {
setProgressValue(prev => Math.min(prev + successValueOffset, 1))
}
return result as ReturnType<Fn>
}, [])

return {
progress,
progressTitle,
progressValue,
}
}

function Progress({
figmaToken,
githubToken,
onError,
}: ProgressProps) {
const navigate = useNavigate()

const [progressValue, setProgressValue] = useState(0)
const [progressText, setProgressText] = useState('')
const {
progress,
progressTitle,
progressValue,
} = useProgress()

const figmaAPI = useFigmaAPI({ token: figmaToken })

Expand All @@ -68,105 +101,143 @@ function Progress({
try {
const { fileKey, ids, nodes } = payload

setProgressText('🚚 피그마에서 svg를 가져오는 중...')
const { images } = await figmaAPI.getSvg({ fileKey, ids })
if (!images) {
throw new Error('선택된 아이콘이 없거나 잘못된 피그마 토큰입니다.')
}
setProgressValue(prev => prev + 0.2)

setProgressText('📦 svg를 파일로 만드는 중...')
const svgBlobs = await Promise.all(
nodes.map(({ id, name }) => fetch(images[id])
.then(response => response.text())
.then(svg => githubAPI
.createGitBlob(svg)
.then(({ sha }) => ({ name, sha })),
)),
)

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

const newSvgBlobs = Object.values(svgBlobsMap)
setProgressValue(prev => prev + 0.3)

setProgressText('📦 svg 파일을 변환하는 중...')
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} 경로가 없습니다. 올바른 경로를 입력했는지 확인해주세요.`)
const getSvgImagesFromFigma = async () => {
const { images } = await figmaAPI.getSvg({ fileKey, ids })
if (!images) {
throw new Error('선택된 아이콘이 없거나 잘못된 피그마 토큰입니다.')
}
parentTrees.push(parentTree)
return githubAPI.getGitTree(targetTree.sha)
}, Promise.resolve(headTree))

const newSvgBlobsTree = [
...prevSvgBlobsTree.map((blob) => {
const overridedBlob = svgBlobsMap[blob.path as string]
if (overridedBlob) {
delete svgBlobsMap[blob.path as string]
return { ...blob, ...overridedBlob }
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} 경로가 없습니다. 올바른 경로를 입력했는지 확인해주세요.`)
}
return null
}).filter(Boolean),
...newSvgBlobs,
]

const newGitSvgTree = await githubAPI.createGitTree({
// @ts-ignore
tree: newSvgBlobsTree,
})
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 newRootGitTree = 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 },
],
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,
})
}, Promise.resolve(newGitSvgTree))
setProgressValue(prev => prev + 0.3)

setProgressText('🚚 PR을 업로드하는 중...')
const now = new Date()
return commit
}

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

const newCommit = await githubAPI.createGitCommit({
message: config.commit.message,
author: {
...config.commit.author,
date: now.toISOString(),
},
parents: [headCommit.sha],
tree: newRootGitTree.sha,
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 newBranchName = `update-icons-${now.valueOf()}`
const svgGitBlobs = await progress({
callback: createSvgGitBlobsFromSvgImages(svgImages),
title: '📦 svg를 파일로 만드는 중...',
successValueOffset: 0.3,
})

await githubAPI.createGitRef({
branchName: newBranchName,
sha: newCommit.sha,
const gitCommit = await progress({
callback: createNewGitCommitFromSvgGitBlobs(svgGitBlobs),
title: '📦 svg 파일을 변환하는 중...',
successValueOffset: 0.3,
})
setProgressValue(prev => prev + 0.2)

const { html_url } = await githubAPI.createPullRequest({
...config.pr,
head: newBranchName,
base: config.repository.baseBranchName,
const pullRequestUrl = await progress({
callback: createGitPullRequestFromGitCommit(gitCommit),
title: '🚚 PR을 업로드하는 중...',
successValueOffset: 0.2,
})

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

navigate('../extract_success', { state: { url: html_url } })
navigate('../extract_success', { state: { url: pullRequestUrl } })
} catch (e: any) {
onError(e?.message)
}
Expand All @@ -194,7 +265,7 @@ function Progress({
/>
</StackItem>
<StackItem>
<Text>{ progressText }</Text>
<Text>{ progressTitle }</Text>
</StackItem>
</VStack>
)
Expand Down
23 changes: 23 additions & 0 deletions packages/bezier-figma-plugin/src/ui/hooks/useGithubAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type CreateGitTreeParameters = RestEndpointMethodTypes['git']['createTree']['par
type CreateGitCommitParameters = RestEndpointMethodTypes['git']['createCommit']['parameters']
type CreateGitRefParameters = RestEndpointMethodTypes['git']['createRef']['parameters']
type CreatePullRequestParameters = RestEndpointMethodTypes['pulls']['create']['parameters']
type AddLabelsRequestParameters = RestEndpointMethodTypes['issues']['addLabels']['parameters']

function useGithubAPI({
auth,
Expand Down Expand Up @@ -149,6 +150,27 @@ function useGithubAPI({
repo,
])

/**
* NOTE: Every pull request is an issue.
* @see https://docs.github.com/en/rest/issues/labels?apiVersion=2022-11-28#about-labels
*/
const addLabels = useCallback(async ({
issueNumber,
labels,
}: { issueNumber: AddLabelsRequestParameters['issue_number'] } & Pick<AddLabelsRequestParameters, 'labels'>) => {
const { data } = await octokit.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels,
})
return data
}, [
octokit,
owner,
repo,
])

return {
getGitCommit,
getGitRef,
Expand All @@ -158,6 +180,7 @@ function useGithubAPI({
createGitRef,
createGitTree,
createPullRequest,
addLabels,
}
}

Expand Down